From c2222c83b69af75262d13a2304e4a3579a1477e1 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Fri, 31 Jul 2026 21:38:27 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20D=C3=A9mo/scenarios=20(2e=20surface)=20:=20la=20TABLE?= =?UTF-8?q?=20des=20sc=C3=A9narios=20=E2=80=94=20les=20trois=20colonnes=20?= =?UTF-8?q?d'IDENTIT=C3=89=20(id=20=C2=B7=20projet+libell=C3=A9=20=C2=B7?= =?UTF-8?q?=20audience)=20=C3=A9taient=20NON=20GAT=C3=89ES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- 05_activity_log/2026-07-31.md | 42 ++++++++++++ ci/README.md | 23 ++++++- ci/check_readme_claims.sh | 123 ++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 0dfe0b8..51de60e 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -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. diff --git a/ci/README.md b/ci/README.md index 3391a10..71b9252 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -361,6 +361,27 @@ dernière citation d'un module (9), ferait dériver le diagramme en silence. Coh croisée en bonus : `counts.modules_cites_uniques` == |`modules_cites`| (liste dédupliquée du manifeste) — mord un compteur désynchronisé de sa propre liste. +**2ᵉ surface `demo/scenarios`** : la **table des scénarios** du MÊME README +(« Scénario | Projet | Audience | Angle ») porte, dans ses trois colonnes +d'identité, des valeurs data-derived de `out/run_sheet.json` (byte-gaté) — +l'`id` (`S-P07-BANQUIER`), le couple `projet`+`projet_libelle` +(`P07 Aqua Terra Las Terrenas`) et l'`audience` (`banquier`) — que le bloc Démo +ci-dessus ne couvrait pas (il ne gate que le compte de modules). Les libellés +projets sont d'ailleurs déclarés « proviennent **verbatim** de CLAUDE.md · +§Projets » sans aucun ancrage. Le gate (1) **ANCRE** chaque `P{code} {libellé}` +du run_sheet à l'entrée correspondante de la section « ## Projets » de `CLAUDE.md` +(verbatim, source unique) ; (2) exige que chaque **ligne** de la table porte +EXACTEMENT id + `P{code} {libellé}` + audience (fin des colonnes wildcard) ; +(3) impose l'**identité d'ensemble** — l'ensemble des ids de lignes == ids du +run_sheet == `counts.scenarios` (aucune ligne fantôme d'un scénario inventé, aucun +scénario manquant). Cohérences croisées en bonus (mordent un artefact interne +incohérent) : `run_sheet` ↔ `MANIFEST` d'accord sur `(id, projet, audience)` · +`id == S-{projet}-{AUDIENCE}` · `counts.scenarios == |scenarios|` · ids non vides +et sans doublon. Sans ce gate, renommer un id, classer un scénario sous le mauvais +projet/audience, ajouter une ligne fantôme ou laisser un libellé projet dériver de +CLAUDE.md restait « vert » alors que le présentateur pitcherait le mauvais couple +projet/audience — le risque même que la run-sheet veut éliminer. + Même classe enfin sur le **montage Chat OTOIA par portail** (livrable `frontend/chat_otoia`) : le nombre de **`Custom Block`**, de **configs runtime** et de **portails** couverts, transcrits **à la main** dans DEUX docs (fiche agent diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index a63996a..6af05c7 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -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=$?