[DTP-Worker] Sprint 8 · buffer L75 · Domaine Frontend/Chat OTOIA : le montage de l'assistant conversationnel par portail (frontend/chat_otoia) affichait « 5 Custom Block + 5 configs runtime » (fiche agent frontend_console) et « 5 portails · 5 fixtures · 5 configs runtime » (README du module) — trois nombres data-derived, TOUS = 5 (piège aveugle), transcrits À LA MAIN dans DEUX docs sans AUCUN gate, alors que frontend/chat_otoia/out/MANIFEST.json (byte-gaté par check_artifacts, counts.custom_blocks/mounts/portails recomptés du contrat RBAC à chaque build via la bijection 1 block ⇔ 1 mount ⇔ 1 portail métier, plateforme exclu) fait autorité. Piège aveugle caractéristique : les trois valent 5 aujourd'hui — un gate qui n'en recompterait qu'UN serait aveugle à la dérive des deux autres (ajouter un portail au contrat RBAC → chat_otoia rebâtit 6 blocks/6 mounts/6 portails pendant que les docs restent à 5 en silence) → nouveau bloc dans check_readme_claims recomputant CHAQUE nombre depuis SON PROPRE champ du MANIFEST et exigeant l'égalité avec les deux docs. Cohérence croisée : custom_blocks == mounts == portails == |MANIFEST.portails| (la bijection du générateur) — mord un compteur désynchronisé de sa propre liste. Un claim absent échoue AUSSI (5 morsures vérifiées : dérive custom_block fiche captée là où mounts reste 5 · dérive configs runtime fiche captée là où blocks reste 5 · dérive portails README · dérive fixtures README · claim configs runtime disparu = red ; restauré = green). ci/README.md (table + détail) mis à jour · 7 gates re-verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-31 12:37:01 +00:00
parent 97a7b25e38
commit 70a8db0f4c
3 changed files with 177 additions and 1 deletions
+97
View File
@@ -72,6 +72,13 @@
# de dépôt » transcrit À LA MAIN dans DEUX docs → confotur
# MANIFEST.counts (fields/data_fields/sections/roles/depot_events, recomptés
# de confotur_spec.json, byte-gatés)
# frontend/chat_otoia/README.md + 03_agents/frontend_console/AGENT.md (montage
# du Chat OTOIA par portail) :
# • triplet « N `Custom Block` · M configs runtime · P portails » (tous = 5
# aujourd'hui — piège aveugle) transcrit À LA MAIN dans DEUX docs → chat_otoia
# MANIFEST.counts (custom_blocks/mounts/portails, recomptés du contrat RBAC,
# byte-gatés) + cohérence croisée == |MANIFEST.portails| (bijection 1 block ⇔
# 1 mount ⇔ 1 portail)
# 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é).
#
@@ -1028,6 +1035,96 @@ if demo_n is not None:
bad(f"Démo · {DEMO_RD} — dit {m.group(1)} modules cités MAIS artefact dit "
f"{demo_n} → régénérer le README")
# ============================================================================
# Chat OTOIA · montage de l'assistant conversationnel par portail (nombre de
# Custom Block / configs runtime / portails) — même classe de « vert trompeur »,
# transcrit À LA MAIN dans DEUX docs, sans AUCUN gate.
# ----------------------------------------------------------------------------
# Le générateur chat_otoia_gen émet out/MANIFEST.json (byte-gaté par
# check_artifacts ⇒ faisant autorité) dont `counts.custom_blocks`, `counts.mounts`
# et `counts.portails` sont RECOMPTÉS à chaque build depuis le contrat RBAC (un
# `Custom Block` ⇔ une config runtime ⇔ un portail métier, `plateforme` exclu —
# invariant 2 du générateur : bijection 1 block ⇔ 1 mount ⇔ 1 portail). Ces
# nombres sont recopiés À LA MAIN : la fiche agent Frontend Console
# (03_agents/frontend_console/AGENT.md — « **N `Custom Block` + M configs
# runtime** ») ET le README du module (05_deliverables_mvp/frontend/chat_otoia/
# README.md — « Portails (N, `plateforme` exclu) », « N fixtures `Custom Block` »,
# « M configs runtime »). Piège « aveugle » caractéristique (cf. DevOps phases
# confirmations, bascule verdict audit_5d) : custom_blocks, mounts ET portails
# valent TOUS TROIS 5 AUJOURD'HUI — un gate qui n'en recompterait qu'un serait
# AVEUGLE à la dérive des autres (ajouter un portail au contrat RBAC → chat_otoia
# rebâtit 6 blocks 6 mounts 6 portails pendant que les docs restent à 5). On
# RECOMPUTE donc CHAQUE nombre depuis SON PROPRE champ du MANIFEST (jamais figé
# ici · zéro duplication du compteur du générateur #5) et on exige l'égalité avec
# les docs. Cohérence croisée : counts.custom_blocks == counts.mounts ==
# counts.portails == |MANIFEST.portails| (la bijection du générateur) — mord un
# compteur désynchronisé de sa propre liste. Un claim absent échoue AUSSI (la
# disparition d'un chiffre est une régression · traçabilité).
# ============================================================================
CO_RD = f"{D}/frontend/chat_otoia/README.md"
CO_FI = "03_agents/frontend_console/AGENT.md"
try:
co_man = load("frontend/chat_otoia/out/MANIFEST.json")
co_cb = str(co_man["counts"]["custom_blocks"])
co_mo = str(co_man["counts"]["mounts"])
co_po = str(co_man["counts"]["portails"])
co_nlist = len(co_man["portails"])
if not (co_cb == co_mo == co_po == str(co_nlist)):
bad(f"Chat OTOIA · incohérence artefact : custom_blocks={co_cb} · "
f"mounts={co_mo} · portails={co_po} · |portails|={co_nlist} "
f"(bijection 1 block ⇔ 1 mount ⇔ 1 portail attendue)")
except (OSError, KeyError, ValueError) as e:
bad(f"Chat OTOIA · artefact chat_otoia illisible/inattendu : {e}"); co_cb = None
if co_cb is not None:
# 1) Fiche agent Frontend Console — « N `Custom Block` + M configs runtime »
co_fi = read_norm(CO_FI)
if co_fi is not None:
m = re.search(r"\*\*(\d+) `Custom Block` \+ (\d+) configs runtime\*\*", co_fi)
if m is None:
bad(f"Chat OTOIA · {CO_FI} — claim « N `Custom Block` + M configs runtime » "
f"INTROUVABLE (attendu {co_cb}/{co_mo})")
elif m.groups() == (co_cb, co_mo):
good(f"Chat OTOIA · {CO_FI} — {'/'.join(m.groups())} == artefact "
f"(custom_blocks={co_cb} · mounts={co_mo})")
else:
bad(f"Chat OTOIA · {CO_FI} — dit {'/'.join(m.groups())} MAIS artefact dit "
f"{co_cb}/{co_mo} → régénérer la fiche")
# 2) README module — « Portails (N, `plateforme` exclu) »
co_rd = read_norm(CO_RD)
if co_rd is not None:
m = re.search(r"Portails \((\d+), `plateforme` exclu\)", co_rd)
if m is None:
bad(f"Chat OTOIA · {CO_RD} — claim « Portails (N, `plateforme` exclu) » "
f"INTROUVABLE (attendu portails={co_po})")
elif m.group(1) == co_po:
good(f"Chat OTOIA · {CO_RD} — {m.group(1)} portails == artefact "
f"(portails={co_po})")
else:
bad(f"Chat OTOIA · {CO_RD} — dit {m.group(1)} portails MAIS artefact dit "
f"{co_po} → régénérer le README")
# 3) README module — « N fixtures `Custom Block` »
m = re.search(r"(\d+) fixtures `Custom Block`", co_rd)
if m is None:
bad(f"Chat OTOIA · {CO_RD} — claim « N fixtures `Custom Block` » INTROUVABLE "
f"(attendu custom_blocks={co_cb})")
elif m.group(1) == co_cb:
good(f"Chat OTOIA · {CO_RD} — {m.group(1)} fixtures == artefact "
f"(custom_blocks={co_cb})")
else:
bad(f"Chat OTOIA · {CO_RD} — dit {m.group(1)} fixtures MAIS artefact dit "
f"{co_cb} → régénérer le README")
# 4) README module — « M configs runtime » (2e transcription des mounts)
m = re.search(r"(\d+) configs runtime", co_rd)
if m is None:
bad(f"Chat OTOIA · {CO_RD} — claim « M configs runtime » INTROUVABLE "
f"(attendu mounts={co_mo})")
elif m.group(1) == co_mo:
good(f"Chat OTOIA · {CO_RD} — {m.group(1)} configs runtime == artefact "
f"(mounts={co_mo})")
else:
bad(f"Chat OTOIA · {CO_RD} — dit {m.group(1)} configs runtime MAIS artefact "
f"dit {co_mo} → régénérer le README")
sys.exit(1 if FAIL else 0)
PY
rc=$?