[DTP-Worker] Sprint 8 · buffer · Démo/scenarios (2e surface) : la TABLE des scénarios — les trois colonnes d'IDENTITÉ (id · projet+libellé · audience) étaient NON GATÉES

Le README `demo/scenarios` porte une table « Scénario | Projet | Audience | Angle »
(README:10-13). Le bloc « Démo » existant de check_readme_claims ne gate QUE
counts.modules_cites_uniques (le compte de modules du diagramme) — ses trois colonnes
d'identité étaient des WILDCARDS, et les libellés projets sont déclarés (README:91-92)
« proviennent verbatim de CLAUDE.md · §Projets » sans AUCUN ancrage vérifié.

Ces colonnes sont DATA-DERIVED de out/run_sheet.json.scenarios[] (id · projet ·
projet_libelle · audience), byte-gaté par check_artifacts. Le générateur ne saisit
aucune donnée métier (résolution RFC 6901 depuis le disque) ; mais check_artifacts
ne prouve QUE run_sheet==build (byte-for-byte) et la source scenario_spec.json (map
projets) recopie les libellés À LA MAIN → toute la chaîne peut DÉRIVER de CLAUDE.md
en restant byte-verte. PREUVE : classer P07 sous audience=client dans le README →
check_readme_claims EXIT 0 (le gate ne voyait que 10==10 modules). Un couple
projet/audience faux ferait pitcher au présentateur le mauvais scénario — le risque
même que la run-sheet veut éliminer — « vert trompeur » qu'aucune suite tests/ (qui
teste des FONCTIONS de résolution, pas la prose) n'attrape.

Nouveau bloc « Démo scénarios » dans ci/check_readme_claims.sh : (1) ANCRAGE — chaque
`P{code} {libellé}` du run_sheet == entrée « ## Projets » de CLAUDE.md (verbatim ·
source unique · même esprit que le catalogue projets dossier_vente) ; (2) TABLE —
chaque ligne porte EXACTEMENT id + `P{code} {libellé}` + audience (fin du wildcard ·
patron identique à la colonne « Type » de roleprofile_gen) ; (3) IDENTITÉ d'ensemble
— {ids des lignes} == {ids du run_sheet} == counts.scenarios (aucune ligne FANTÔME,
aucun scénario MANQUANT). Cohérences croisées en bonus (mordent un artefact
INTERNEMENT incohérent) : run_sheet ↔ MANIFEST d'accord sur (id,projet,audience) ·
id == S-{projet}-{AUDIENCE} · counts.scenarios == |scenarios| · ids non vides/sans
doublon. Un claim absent échoue AUSSI.

8 morsures vérifiées : prose renomme un id (INTROUVABLE) · prose classe P07 sous
audience=client · prose met le mauvais libellé projet · ligne FANTÔME S-P99-GHOST
(en trop) · artefact libellé « Aqua Terra Bay » DÉRIVE de CLAUDE.md · run_sheet↔
MANIFEST désync audience · counts.scenarios=3 (≠|scenarios|=2) · ligne S-P05-CLIENT
supprimée (MANQUANT) ; restauré = green : 2 lignes == run_sheet == counts (2) ·
libellés == CLAUDE.md §Projets · exit 0. État courant : aucune valeur périmée
(anti-invention #6, rien à réécrire) — le défaut est la surface ungated. ci/README.md
(table + détail « 2e surface demo/scenarios ») mis à jour · working tree byte-restauré
· 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 21:38:27 +00:00
parent 6f2a3c57ae
commit c2222c83b6
3 changed files with 187 additions and 1 deletions
+42
View File
@@ -2405,3 +2405,45 @@ verrouillés) · `guard_constraints` · `validate_json`.
`check_regression` · `guard_constraints` · `validate_json`).
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo).
- **Auto-score 4Big** : 96/100.
## Sprint 8 · buffer · Démo/scenarios (2e surface) · la TABLE des scénarios · identité (id · projet+libellé · audience)
- **Constat** : le README `demo/scenarios` porte une table « Scénario | Projet |
Audience | Angle » (README:10-13) dont les **trois colonnes d'identité** sont
data-derived : `id` (`S-P07-BANQUIER`), `projet`+`projet_libelle`
(`P07` + `Aqua Terra Las Terrenas`) et `audience` (`banquier`). Le bloc « Démo »
existant de `check_readme_claims.sh` ne gate QUE `counts.modules_cites_uniques`
(le compte de modules du diagramme) — ces colonnes étaient des **wildcards**. Les
libellés projets sont même déclarés (README:91-92) « proviennent **verbatim** de
CLAUDE.md · §Projets » sans AUCUN ancrage vérifié.
- **Data-derived** : `out/run_sheet.json.scenarios[]` (`id`/`projet`/
`projet_libelle`/`audience`), byte-gaté par `check_artifacts`. Le générateur ne
saisit aucune donnée métier (résolution RFC 6901 depuis le disque). `check_artifacts`
ne prouve QUE `run_sheet==build` (byte-for-byte) ; la source `scenario_spec.json`
(map `projets`) recopie les libellés à la main → toute la chaîne peut DÉRIVER de
CLAUDE.md en restant byte-verte.
- **Preuve reproduite** : classer P07 sous `audience=client` dans le README →
`check_readme_claims` **exit 0** (le gate ne voyait que `10 == 10` modules).
- **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Démo scénarios ») : (1)
**ANCRAGE** — chaque `P{code} {libellé}` du run_sheet == entrée « ## Projets » de
CLAUDE.md (verbatim, source unique · même esprit que le catalogue projets
dossier_vente) ; (2) **TABLE** — chaque ligne porte EXACTEMENT id +
`P{code} {libellé}` + audience (fin du wildcard, patron identique à la colonne
« Type » de roleprofile_gen) ; (3) **IDENTITÉ d'ensemble** — {ids des lignes} ==
{ids du run_sheet} == `counts.scenarios` (aucune ligne FANTÔME, aucun scénario
MANQUANT). Cohérences croisées en bonus (mordent un artefact INTERNEMENT
incohérent) : `run_sheet` ↔ `MANIFEST` d'accord sur `(id, projet, audience)` ·
`id == S-{projet}-{AUDIENCE}` · `counts.scenarios == |scenarios|` · ids non vides
et sans doublon. Un claim absent échoue AUSSI.
- **8 morsures vérifiées** : prose renomme un id (INTROUVABLE) · prose classe P07
sous audience=client · prose met le mauvais libellé projet · ligne FANTÔME
`S-P99-GHOST` (en trop) · artefact libellé « Aqua Terra Bay » DÉRIVE de CLAUDE.md ·
`run_sheet`↔`MANIFEST` désync audience · `counts.scenarios=3 (≠|scenarios|=2)` ·
ligne `S-P05-CLIENT` supprimée (MANQUANT). Restauré = green : 2 lignes ==
run_sheet == counts (2) · libellés == CLAUDE.md §Projets · exit 0.
- **État courant** : aucune valeur périmée (anti-invention #6, rien à réécrire) — le
défaut est la surface **ungated**. `ci/README.md` (table + détail « 2ᵉ surface
`demo/scenarios` ») mis à jour · working tree byte-restauré · **7 gates re-verts**
(`check_readme_claims` · `check_artifacts` · `check_docs` · `check_ci_integrity` ·
`check_regression` · `guard_constraints` · `validate_json`).
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo).
- **Auto-score 4Big** : 96/100.
+22 -1
View File
File diff suppressed because one or more lines are too long
+123
View File
@@ -2446,6 +2446,129 @@ if exp is not None:
bad(f"Faisabilité canoniques · {FG_RD} — prose énumère {sorted(got)} MAIS "
f"CLAUDE.md {sorted(want)}{detail} → régénérer le README")
# ============================================================================
# Démo · run-sheet de pitch (2e surface du MÊME README) · la TABLE des SCÉNARIOS
# — l'IDENTITÉ de chaque ligne (id · projet+libellé · audience), jamais gatée.
# ----------------------------------------------------------------------------
# Le bloc « Démo » ci-dessus recompute le seul COMPTE de modules cités du
# diagramme. Restait, dans le MÊME README, une surface data-derived distincte et
# NON gatée : la table « Scénario | Projet | Audience | Angle » (README:10-13).
# Ses trois colonnes d'identité sont DATA-DERIVED de out/run_sheet.json (byte-
# gaté par check_artifacts ⇒ faisant autorité) : `id` (« S-P07-BANQUIER »),
# `projet`+`projet_libelle` (« P07 » + « Aqua Terra Las Terrenas ») et `audience`
# (« banquier »). Le générateur ne saisit AUCUNE donnée métier — il résout depuis
# le disque ; les libellés projets sont d'ailleurs déclarés (README:91-92)
# « proviennent **verbatim** de CLAUDE.md · §Projets ». Or AUCUN gate ne le
# vérifiait : check_artifacts ne prouve QUE run_sheet==build (byte-for-byte) et
# le bloc Démo existant ne gate QUE `modules_cites_uniques` → RENOMMER un id dans
# la prose · CLASSER un scénario sous le mauvais projet/audience · AJOUTER une
# ligne fantôme d'un scénario inventé · laisser un libellé projet DÉRIVER de
# CLAUDE.md (« Aqua Terra » → « Aqua Terra Bay ») restait « vert trompeur »
# qu'aucune suite tests/ (qui teste des FONCTIONS de résolution, pas la prose du
# README) n'attrape → un présentateur pitcherait le mauvais projet/audience (le
# risque même que la run-sheet veut éliminer). Même patron EXACT que la colonne
# « Type » de roleprofile_gen (identité de table, fin du wildcard) et l'ancrage
# du catalogue projets dossier_vente à CLAUDE.md §Projets. On RECOMPUTE : (1)
# ANCRAGE — chaque `P{code} {libelle}` du run_sheet == l'entrée CLAUDE.md §Projets
# de ce code (verbatim, comme la prose l'affirme · zéro duplication) ; (2) TABLE —
# chaque ligne porte EXACTEMENT id + `P{code} {libelle}` + audience ; (3) IDENTITÉ
# d'ensemble — l'ensemble des ids de lignes == l'ensemble des ids du run_sheet ==
# counts.scenarios (aucune ligne FANTÔME, aucun scénario MANQUANT). Cohérences
# croisées en bonus (mordent un artefact INTERNEMENT incohérent) : run_sheet ↔
# MANIFEST d'accord sur (id, projet, audience) · id == f"S-{projet}-{AUDIENCE}" ·
# counts.scenarios == |scenarios| · ids non vides et sans doublon. Un claim absent
# échoue AUSSI (disparition = régression).
# ============================================================================
DEMO_RD2 = f"{D}/demo/scenarios/README.md"
try:
d_rs = load("demo/scenarios/out/run_sheet.json")["scenarios"]
d_man = load("demo/scenarios/out/MANIFEST.json")
d_man_sc = d_man["scenarios"]
d_n = int(d_man["counts"]["scenarios"])
except (OSError, KeyError, ValueError, TypeError) as e:
bad(f"Démo scénarios · artefact run_sheet/MANIFEST illisible/inattendu : {e}"); d_rs = None
# ancrage libellés projets : CLAUDE.md §Projets = source faisant autorité
try:
_cmp = open("CLAUDE.md", encoding="utf-8").read()
_mp = re.search(r"^## Projets\s*\n(.+)$", _cmp, re.M)
if _mp is None:
raise ValueError("section « ## Projets » INTROUVABLE")
_proj_map = {}
for _t in _mp.group(1).split("·"):
_t = _nfc(_t.strip())
_mc = re.match(r"(P\d{2})\b", _t)
if _mc:
_proj_map[_mc.group(1)] = _t
if not _proj_map:
raise ValueError("aucun projet PNN parsé")
except (OSError, ValueError) as e:
bad(f"Démo scénarios · CLAUDE.md §Projets illisible/inattendu : {e}"); _proj_map = None
if d_rs is not None and _proj_map is not None:
d_rd = read_norm(DEMO_RD2)
# cohérences croisées internes à l'artefact (avant de croire la prose)
rs_ids = [s["id"] for s in d_rs]
if len(set(rs_ids)) != len(rs_ids) or "" in rs_ids:
bad(f"Démo scénarios · run_sheet incohérent : ids={rs_ids} "
f"(doublon ou vide)")
if d_n != len(d_rs):
bad(f"Démo scénarios · incohérence artefact : counts.scenarios={d_n} "
f"MAIS |scenarios|={len(d_rs)}")
man_key = {s["id"]: (s["projet"], s["audience"]) for s in d_man_sc}
for s in d_rs:
sid, proj, aud = s["id"], s["projet"], s["audience"]
# id == f"S-{projet}-{AUDIENCE}" (encode projet+audience)
exp_id = f"S-{proj}-{aud.upper()}"
if sid != exp_id:
bad(f"Démo scénarios · run_sheet incohérent : id={sid!r} MAIS "
f"projet={proj}/audience={aud} imposent {exp_id!r}")
# run_sheet ↔ MANIFEST d'accord sur (projet, audience)
if man_key.get(sid) != (proj, aud):
bad(f"Démo scénarios · run_sheet ↔ MANIFEST désynchronisés pour {sid} : "
f"run_sheet=({proj},{aud}) MANIFEST={man_key.get(sid)}")
# ANCRAGE : libellé projet == CLAUDE.md §Projets (verbatim, cf. README:91)
full = _nfc(f"{proj} {s['projet_libelle']}")
anchor = _proj_map.get(proj)
if anchor is None:
bad(f"Démo scénarios · projet {proj} du run_sheet ABSENT de CLAUDE.md "
f"§Projets (libellé « {full} » non ancré)")
elif _ai(full) != _ai(anchor):
bad(f"Démo scénarios · libellé projet « {full} » du run_sheet DÉRIVE de "
f"CLAUDE.md §Projets « {anchor} » → aligner (ou CLAUDE.md)")
else:
good(f"Démo scénarios · {sid} — libellé « {full} » == CLAUDE.md §Projets")
# TABLE : ligne README porte id + `P{code} {libelle}` + audience
if d_rd is not None:
pat = (rf"\| `{re.escape(sid)}` \| \*\*([^|]+?)\*\* \| "
r"([^|]+?) \|")
m = re.search(pat, d_rd)
if m is None:
bad(f"Démo scénarios · {DEMO_RD2} — ligne `{sid}` INTROUVABLE "
f"(attendu projet « {full} » · audience « {aud} »)")
else:
got_lbl, got_aud = m.group(1).strip(), m.group(2).strip()
if _ai(got_lbl) != _ai(full):
bad(f"Démo scénarios · {sid} — {DEMO_RD2} porte projet "
f"« {got_lbl} » MAIS run_sheet dit « {full} » → régénérer")
elif got_aud != aud:
bad(f"Démo scénarios · {sid} — {DEMO_RD2} porte audience "
f"« {got_aud} » MAIS run_sheet dit « {aud} » → régénérer")
else:
good(f"Démo scénarios · {sid} — ligne README (projet « {got_lbl} » · "
f"audience « {got_aud} ») == run_sheet")
# IDENTITÉ d'ensemble : ids des lignes de la table == ids du run_sheet (== counts)
if d_rd is not None:
row_ids = set(re.findall(r"\| `(S-[^`]+)` \| \*\*[^|]+\*\* \| [^|]+ \|", d_rd))
want_ids = set(rs_ids)
if row_ids == want_ids and len(row_ids) == d_n:
good(f"Démo scénarios · table — {len(row_ids)} lignes == run_sheet == "
f"counts.scenarios ({d_n}) : aucun scénario FANTÔME ni MANQUANT")
else:
miss = sorted(want_ids - row_ids); extra = sorted(row_ids - want_ids)
detail = (f" · absents={miss}" if miss else "") + \
(f" · en trop={extra}" if extra else "")
bad(f"Démo scénarios · table — lignes {sorted(row_ids)} MAIS run_sheet "
f"{sorted(want_ids)} (counts.scenarios={d_n}){detail} → régénérer le README")
sys.exit(1 if FAIL else 0)
PY
rc=$?