2026-07-23 15:49:16 -04:00
|
|
|
|
#!/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
|
|
|
|
|
|
|
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas
La revision a commence par un balayage par motifs — chemins morts, cibles make
absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque
tout le reste : un motif ne voit que ce qui s exprime en motif.
make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait
au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par
AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus
haut. Il fallait lire pour la voir.
74 documents lus un par un. 66 corriges, 8 exacts.
CE QUI ETAIT FRANCHEMENT FAUX
AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre
des VM reelles. Elle a ete rasee et remontee depuis zero trois fois.
ecosysteme-chezlepro.md, le document montre a un client, portait la meme
phrase : il se sous-vendait gravement.
courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de
son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n
est construit alors qu il rapporte des mesures datees du role en fonctionnement.
hebergeur-exploitation.md disait rien n est fait d un depot qui existe.
filiation-emancipation.md se contredisait a deux ecrans de distance.
DES MODELES DECRITS D APRES UN MONDE ANTERIEUR
Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le
donnaient en exemple d integration FACULTATIVE — il est universel depuis le
2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID
a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki
qui avait raison.
CE QUI CASSE AU PREMIER ESSAI
Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le
FABRIQUE et le critere R2 de l epreuve d operateur independant.
preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait
detruite : raser derive du plan, il ne la detruira jamais — le risque est l
inverse. Un mot de passe d essai en clair dans un depot public.
DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME
P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d
un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux
declarations reelles : 12 annonces, 21 reels.
Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la
conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une
erreur ajoute l assurance a l erreur.
CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT
Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les
meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il
nomme existe. P29 tient les positions d authentification, personne ne tient les
habilitations.
make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
|
|
|
|
from urllib.parse import quote
|
2026-07-23 15:49:16 -04:00
|
|
|
|
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("")
|
documentation : la tournee des 74 documents, parce qu un balayage ne lit pas
La revision a commence par un balayage par motifs — chemins morts, cibles make
absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque
tout le reste : un motif ne voit que ce qui s exprime en motif.
make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait
au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par
AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus
haut. Il fallait lire pour la voir.
74 documents lus un par un. 66 corriges, 8 exacts.
CE QUI ETAIT FRANCHEMENT FAUX
AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre
des VM reelles. Elle a ete rasee et remontee depuis zero trois fois.
ecosysteme-chezlepro.md, le document montre a un client, portait la meme
phrase : il se sous-vendait gravement.
courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de
son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n
est construit alors qu il rapporte des mesures datees du role en fonctionnement.
hebergeur-exploitation.md disait rien n est fait d un depot qui existe.
filiation-emancipation.md se contredisait a deux ecrans de distance.
DES MODELES DECRITS D APRES UN MONDE ANTERIEUR
Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le
donnaient en exemple d integration FACULTATIVE — il est universel depuis le
2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID
a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki
qui avait raison.
CE QUI CASSE AU PREMIER ESSAI
Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le
FABRIQUE et le critere R2 de l epreuve d operateur independant.
preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait
detruite : raser derive du plan, il ne la detruira jamais — le risque est l
inverse. Un mot de passe d essai en clair dans un depot public.
DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME
P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d
un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux
declarations reelles : 12 annonces, 21 reels.
Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la
conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une
erreur ajoute l assurance a l erreur.
CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT
Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les
meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il
nomme existe. P29 tient les positions d authentification, personne ne tient les
habilitations.
make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-06 16:18:23 -04:00
|
|
|
|
# LIEN RELATIF AU FICHIER GENERE, PAS AU WIKI (2026-09-06). Le lien nu `<stem>`
|
|
|
|
|
|
# est la convention du wiki Forgejo, ou les pages sont freres. Mais ce fichier-ci
|
|
|
|
|
|
# vit dans `docs/audit/` : les 22 liens ainsi produits ne menaient nulle part
|
|
|
|
|
|
# depuis le depot. Un plan de recette dont chaque source est un lien mort se lit
|
|
|
|
|
|
# une fois, puis plus jamais.
|
|
|
|
|
|
L.append(f"*Source : [{titre}](../../wiki/{quote(stem)}.md) · § À toi de jouer.*")
|
2026-07-23 15:49:16 -04:00
|
|
|
|
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())
|