Set-OPS-Public/scripts/plan_recette.py
Daniel Allaire 58015f56e0 Plan de recette (P22) : le pendant manuel de make prouver
Les 78 exercices « À toi de jouer » du wiki forment un plan de tests d'acceptation.
Formalisé sans dupliquer :

- scripts/plan_recette.py + make plan-recette : GÉNÈRE docs/audit/plan-de-recette.md
  depuis les exercices du wiki. Grille auto-contenue par unité, colonnes : ce qu'on
  éprouve · le geste · type (observe/casse-répare) · Preuve auto (le Pxx extrait du
  texte -> quels gestes manuels sont AUSSI gardés par la machine). Générée -> ne peut
  pas dériver du wiki.
- Preuve P22 : plan_recette.py --verifier échoue si le fichier committé est périmé.
  Le plan de recette devient auto-gardé.
- Honnêteté de couverture assumée : « — » = manuel seul ; pas d'exhaustivité au-delà
  des exercices du wiki.

Pendant humain de make prouver (le harnais prouve le moteur P01-P21, la recette valide
l'exploitation) ; checklist du protocole-operateur-independant (« exploitable sans IA »).

Validé : 78 gestes / 19 unités, 5 doublés d'un Pxx ; P22 détecte une dérive (testé) ;
make verifier -> CONFORME 22/22 (instance cohérente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:49:16 -04:00

146 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 (P01P21), 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())