#!/usr/bin/env python3 """Génère le PLAN DE RECETTE (tests d'acceptation manuels) depuis les exercices du wiki. Le wiki porte, dans chaque unité, une section « ④ À toi de jouer » : des exercices hands-on (observe / casse-répare). Pris ensemble, ils forment un **plan de recette** manuel — le pendant humain de `make prouver` (qui, lui, prouve le moteur par machine). Ce script LIT ces exercices et produit `docs/audit/plan-de-recette.md` : une grille auto-contenue (le geste est inline), avec une colonne **Preuve auto** extraite du texte (le `Pxx` que l'exercice mentionne, quand un garde-fou machine couvre AUSSI le geste). La grille étant GÉNÉRÉE, elle ne peut pas dériver du wiki : `--verifier` échoue si le fichier committé n'est plus à jour (à régénérer par `make plan-recette`). Usage : python3 scripts/plan_recette.py # (ré)génère docs/audit/plan-de-recette.md python3 scripts/plan_recette.py --verifier # rc=2 si le fichier committé est périmé """ from __future__ import annotations import re import sys from pathlib import Path RACINE = Path(__file__).resolve().parents[1] WIKI = RACINE / "wiki" SORTIE = RACINE / "docs" / "audit" / "plan-de-recette.md" def _titre(md: str) -> str: m = re.search(r"(?m)^#\s+(.+)$", md) return m.group(1).strip() if m else "?" def _section_exercices(md: str) -> str | None: m = re.search(r"## ④ À toi de jouer(.*?)(?:\n## |\Z)", md, re.S) return m.group(1) if m else None def _items(section: str) -> list[str]: """Découpe la section en items numérotés (chacun peut contenir du multi-ligne / code).""" bornes = [m.start() for m in re.finditer(r"(?m)^\d+\.\s", section)] bornes.append(len(section)) return [section[bornes[i]:bornes[i + 1]].strip() for i in range(len(bornes) - 1)] def _nettoyer(texte: str) -> tuple[str, str]: """(intro, geste) : l'intro = 1er gras (le titre de l'exercice) ; le geste = le reste, nettoyé.""" corps = re.sub(r"^\d+\.\s*", "", texte) mi = re.match(r"\*\*(.+?)\.?\*\*\s*", corps) intro = mi.group(1).strip() if mi else "" reste = corps[mi.end():] if mi else corps reste = re.sub(r"```.*?```", "« commande »", reste, flags=re.S) # blocs de code reste = re.sub(r"`([^`]+)`", r"\1", reste) # code inline reste = re.sub(r"\*\*(.+?)\*\*", r"\1", reste) # gras reste = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", reste) # liens reste = re.sub(r"-{3,}", " ", reste) # filets --- de fin de section reste = re.sub(r"\s+", " ", reste).strip() if len(reste) > 240: reste = reste[:237].rstrip() + "…" return intro, reste def _type(texte: str) -> str: return "🔨 casse-répare" if re.search(r"[Cc]asse\s*&?\s*répare", texte) else "👁 observe" def _preuves(texte: str) -> str: px = sorted(set(re.findall(r"P\d{2}", texte))) return " ".join(f"**{p}**" for p in px) if px else "—" def generer() -> str: unites = [] for f in sorted(WIKI.glob("*.md")): md = f.read_text(encoding="utf-8") sec = _section_exercices(md) if not sec: continue items = _items(sec) if items: unites.append((_titre(md), f.stem, items)) total = sum(len(it) for _, _, it in unites) auto = sum(1 for _, _, its in unites for it in its if re.search(r"P\d{2}", it)) casse = sum(1 for _, _, its in unites for it in its if "casse" in it.lower()) L = [] L.append("# Plan de recette — tests d'acceptation manuels") L.append("") L.append("> **GÉNÉRÉ** depuis les exercices du wiki (`wiki/*.md`, section « ④ À toi de jouer »)") L.append("> par `scripts/plan_recette.py` — **ne pas éditer à la main** ; régénérer avec") L.append("> `make plan-recette`. La preuve **P22** échoue si ce fichier n'est plus à jour.") L.append("") L.append("Ce plan est le **pendant manuel** de `make prouver` : là où le harnais prouve le") L.append("*moteur* par machine (P01–P21), ces gestes valident l'*exploitation* — ce qu'un") L.append("**humain** fait, voit, casse et répare. Ensemble, ils couvrent les deux moitiés ;") L.append("le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.") L.append("`protocole-operateur-independant.md`).") L.append("") L.append(f"**{total} gestes** sur **{len(unites)} unités** · **{casse}** en « casse-répare »") L.append(f"· **{auto}** doublés d'un garde-fou machine (colonne *Preuve auto*).") L.append("") L.append("> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une") L.append("> preuve `Pxx` de `make prouver` couvre AUSSI le geste. Un « — » signifie **manuel") L.append("> seul** : aucune machine ne le garde, seul l'œil de l'opérateur le valide. Ce plan") L.append("> ne prétend pas à l'exhaustivité au-delà des exercices présents dans le wiki.") L.append("") for titre, stem, items in unites: L.append(f"## {titre}") L.append("") L.append("| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |") L.append("|---|---|---|---|---|") for i, it in enumerate(items, 1): intro, geste = _nettoyer(it) geste = geste.replace("|", "\\|") intro = intro.replace("|", "\\|") L.append(f"| {i} | {intro or '—'} | {geste} | {_type(it)} | {_preuves(it)} |") L.append("") L.append(f"*Source : [{titre}]({stem}) · § À toi de jouer.*") L.append("") return "\n".join(L).rstrip() + "\n" def main() -> int: verifier = "--verifier" in sys.argv[1:] attendu = generer() if verifier: actuel = SORTIE.read_text(encoding="utf-8") if SORTIE.exists() else "" if actuel != attendu: print("erreur: docs/audit/plan-de-recette.md est PÉRIMÉ (le wiki a changé). " "Régénérer : make plan-recette.", file=sys.stderr) return 2 n = attendu.count("\n| ") print(f"Plan de recette à jour ({attendu.count('## ') - 0} sections).") return 0 SORTIE.write_text(attendu, encoding="utf-8") print(f"Écrit : {SORTIE.relative_to(RACINE)}") return 0 if __name__ == "__main__": raise SystemExit(main())