[DTP-Worker] Sprint 8 · buffer L75 · Hors du domaine « tests » : les comptes PAR WORKSPACE (cartes/liens/rôles) des 5 portails frontend étaient transcrits À LA MAIN dans DEUX docs (table du README module + prose de la fiche frontend_console) sans AUCUN gate — ajouter un rôle/DocType au contrat RBAC ferait dériver les deux docs en silence pendant que frontend/portails/out/MANIFEST.json (byte-gaté par check_artifacts) dit autre chose → nouveau bloc dans check_readme_claims recomputant chaque triplet + le total « N Workspace natifs » depuis MANIFEST.workspaces et exigeant l'égalité avec les deux docs (4 morsures : dérive table · dérive prose captée là où la table est aveugle · cellule disparue · total)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-31 08:06:10 +00:00
parent d7fc6bab26
commit 56e64d6089
3 changed files with 155 additions and 1 deletions
+77
View File
@@ -42,6 +42,11 @@
# • « Total CRM N (a+b+c) » · « RBAC N (…) » · « e-CF N » — total ET multiset
# des composants recomputés (une compensation entre suites laisserait la
# table juste et la prose fausse) → plan.suites[path]
# frontend/portails/README.md + 03_agents/frontend_console/AGENT.md :
# • comptes PAR WORKSPACE (cartes / liens / rôles) transcrits À LA MAIN dans
# DEUX docs (table du README + prose « OTO Ventes 4 cartes/11 liens/12
# rôles ») + total « N Workspace natifs » → portails MANIFEST
# (tableau `workspaces`, byte-gaté par check_artifacts)
# Un claim absent échoue AUSSI (la dérive de formulation qui ferait disparaître
# un chiffre est elle-même une régression de traçabilité).
#
@@ -384,6 +389,78 @@ else:
bad(f"Prose agrégée · e-CF — dit {mecf.group(1)} MAIS source dit {ecf_exp} "
f"(fiscal/ecf_dgii) → régénérer la fiche")
# ============================================================================
# Portails frontend · comptes PAR WORKSPACE (cartes / liens / rôles) —
# même classe de « vert trompeur », transcrits À LA MAIN dans DEUX docs.
# ----------------------------------------------------------------------------
# Le générateur de portails émet out/MANIFEST.json dont le tableau `workspaces`
# porte, PAR portail, {nb_cartes, nb_liens, nb_roles} recalculés depuis le
# contrat RBAC à chaque build (byte-gaté par check_artifacts). Ces triplets sont
# recopiés À LA MAIN à DEUX endroits : la table du README du module
# (frontend/portails/README.md, colonnes Cartes|Liens|Rôles) ET la prose de la
# fiche agent (03_agents/frontend_console/AGENT.md — « OTO Ventes 4 cartes/11
# liens/12 rôles · … »). Aucun gate ne les couvrait : ajouter un rôle/DocType au
# contrat ferait dériver LES DEUX docs en silence pendant que l'artefact dit
# autre chose — même piège que les cellules « Tests » et la prose agrégée. On
# RECOMPUTE ici chaque triplet depuis MANIFEST (déjà byte-gaté · zéro duplication
# du compteur du générateur) et on exige l'égalité avec les DEUX docs. Un
# workspace absent d'un doc échoue AUSSI (traçabilité).
# ============================================================================
WS_RD = f"{D}/frontend/portails/README.md"
WS_FI = "03_agents/frontend_console/AGENT.md"
try:
ws_list = load("frontend/portails/out/MANIFEST.json")["workspaces"]
except (OSError, KeyError, ValueError) as e:
bad(f"Portails · MANIFEST illisible/inattendu : {e}"); ws_list = []
try:
ws_rd = re.sub(r"\s+", " ", open(WS_RD, encoding="utf-8").read())
except OSError as e:
bad(f"Portails · {WS_RD} illisible : {e}"); ws_rd = ""
try:
ws_fi = re.sub(r"\s+", " ", open(WS_FI, encoding="utf-8").read())
except OSError as e:
bad(f"Portails · {WS_FI} illisible : {e}"); ws_fi = ""
for w in ws_list:
portail, ws = w["portail"], w["workspace"]
exp = (str(w["nb_cartes"]), str(w["nb_liens"]), str(w["nb_roles"]))
src = f"cartes={exp[0]} liens={exp[1]} rôles={exp[2]}"
# 1) Table du README du module : | <portail> | `<ws>` | C | L | R |
rd_pat = (rf"\| {re.escape(portail)} \| `{re.escape(ws)}` \| "
r"(\d+) \| (\d+) \| (\d+) \|")
mrd = re.search(rd_pat, ws_rd)
if mrd is None:
bad(f"Portails README · {ws} — ligne INTROUVABLE dans {WS_RD} (attendu {src})")
elif mrd.groups() == exp:
good(f"Portails README · {ws} — {'/'.join(mrd.groups())} == MANIFEST ({src})")
else:
bad(f"Portails README · {ws} — {WS_RD} dit {'/'.join(mrd.groups())} MAIS "
f"MANIFEST dit {'/'.join(exp)} ({src}) → régénérer le README")
# 2) Prose de la fiche agent : `<ws>` C cartes/L liens/R rôles (ou C/L/R)
fi_pat = (rf"`{re.escape(ws)}` (\d+)(?: cartes)?/(\d+)(?: liens)?/"
r"(\d+)(?: rôles)?")
mfi = re.search(fi_pat, ws_fi)
if mfi is None:
bad(f"Portails fiche · {ws} — claim INTROUVABLE dans {WS_FI} (attendu {src})")
elif mfi.groups() == exp:
good(f"Portails fiche · {ws} — {'/'.join(mfi.groups())} == MANIFEST ({src})")
else:
bad(f"Portails fiche · {ws} — {WS_FI} dit {'/'.join(mfi.groups())} MAIS "
f"MANIFEST dit {'/'.join(exp)} ({src}) → régénérer la fiche")
# Total de workspaces (« N `Workspace` natifs ») cohérent artefact ↔ fiche.
n_ws = len(ws_list)
if n_ws:
mtot = re.search(r"\*\*(\d+) `Workspace` natifs", ws_fi)
if mtot is None:
bad(f"Portails fiche · total — « N `Workspace` natifs » INTROUVABLE "
f"dans {WS_FI} (attendu {n_ws})")
elif int(mtot.group(1)) == n_ws:
good(f"Portails fiche · total — {mtot.group(1)} == MANIFEST ({n_ws} workspaces)")
else:
bad(f"Portails fiche · total — {WS_FI} dit {mtot.group(1)} MAIS "
f"MANIFEST dit {n_ws} → régénérer la fiche")
sys.exit(1 if FAIL else 0)
PY
rc=$?