Set-OPS-Public/scripts/plan_recette.py
Daniel Allaire 5bc3bceac1
Some checks failed
verifier / verifier (push) Has been cancelled
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

152 lines
6.7 KiB
Python
Raw Permalink 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
from urllib.parse import quote
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("")
# 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.*")
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())