# Activity log · 2026-08-02 · Claude Code DTP Worker ## Sprint 8 · buffer · Faisabilité — ancrage du DOMAINE DES CODES PROJET (garde d'entrée `_validate_brief` + 2 schémas JSON) sur CLAUDE.md §Projets **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La classe « constante constitutionnelle recopiée dans une GARDE DE VALIDATION en code, jamais ancrée » (déjà couverte pour le CONTRAT DE FORMAT e-NCF de `ncf.py`) portait un candidat resté NON gaté et paradoxalement au **cœur du Livrable Sprint 3** (« P07 Aqua Terra <1h ») : le **DOMAINE des codes projet valides** du générateur 4 volets. Ce domaine EST exactement l'ensemble `CLAUDE.md §Projets` (P01..P09), recopié À LA MAIN à **QUATRE surfaces**, aucune ancrée, toutes invisibles à `check_artifacts` (ni `.py` ni schémas ne sont des `out/*.json`) : 1. `faisabilite/generator/faisabilite_gen.py::_validate_brief` — la GARDE runtime `code[:2] == "P0" and code[2] in "123456789"` (le domaine EFFECTIF) + le message `« … invalide (attendu P01..P09) »` (les bornes) ; 2. `faisabilite/version.schema.json` — `properties.projet.pattern "^P0[1-9]$"` (validation du `version.json` par projet) + sa `description` ; 3. `faisabilite/generator/brief.schema.json` — même `pattern "^P0[1-9]$"` (validation du brief d'entrée) + sa `description` (qui n'en avait **aucune**). **Le « vert trompeur »** — si CLAUDE.md §Projets AJOUTE un 10ᵉ projet (`P10 …`, évènement de portefeuille réel), la garde ET les deux patterns REJETTENT `P10` comme « invalide (attendu P01..P09) » : le générateur **REFUSE de scaffolder un projet LÉGITIMEMENT mandaté** (bug d'exécution) pendant que la suite `tests/` reste VERTE (oracles P01/P02 self-consistants, jamais comparés à §Projets). Symétriquement, RETIRER un projet laisse la garde trop PERMISSIVE (accepte un code démobilisé). Ni `check_artifacts` (schémas/`.py` ≠ `out/*.json`) ni `tests/` (teste la FONCTION `_validate_brief`, pas son ANCRAGE à §Projets) ne mordent. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Faisabilité/generator · DOMAINE DES CODES PROJET », juste après « Fiches #10 ») — RE-DÉRIVE le catalogue de codes depuis `CLAUDE.md §Projets` (`\bP\d{2,}\b` sur la ligne `## Projets` · source unique) puis exige que chaque surface accepte **EXACTEMENT** cet ensemble : (a) la garde `_validate_brief` — reconstruit l'ensemble ACCEPTÉ `{préfixe+chiffre}` depuis le code source et set-diff == §Projets ; le message cite les bornes min..max ; (b)/(c) les 2 patterns JSON — **test COMPORTEMENTAL** (compile le pattern, l'applique à une sonde `§Projets ∪ {P00,P10,P99,P0,PXX,P1,P010,Q01}`, exige accepté == §Projets · robuste à la forme de la classe de caractères) + la `description` DÉCLARE l'ancrage « CLAUDE.md §Projets » et cite les bornes. Une surface INTROUVABLE échoue AUSSI (ancre morte · anti-évaporation #6). Deux descriptions de schéma enrichies pour porter l'ancrage (brief.schema en gagne une). **Vérif** — 6 checks verts sur l'arbre propre. **6 morsures** adversariales : M1 garde retire le chiffre `7` (validateur trop restrictif) ⇒ set ≠ · M2 message `P01..P09→P01..P08` ⇒ bornes ≠ · M3 pattern version élargi `^P0[0-9]$` ⇒ accepte `P00` en trop · M4 brief.schema perd l'ancrage §Projets ⇒ ne DÉCLARE plus · **M5 SILENT-GREEN : CLAUDE.md §Projets ajoute `P10` SEUL (les 4 surfaces inchangées) ⇒ CASCADE sur les 6 lignes (garde accepte≠, 2 patterns manquent P10, message +2 descriptions bornes P09≠P10) — preuve que l'autorité est §Projets, pas la cohérence interne** · M6 version.schema perd les bornes de sa description. Les **16 tests unittest** du générateur restent verts (garde inchangée). Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. --- ## Sprint 8 · buffer · RBAC SPEC §3 — ancrage de la colonne « Familles rattachées » PAR portail sur le contrat **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). La table §3 « Cartographie portails ↔ familles » de `05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` porte DEUX colonnes data-derived du contrat `rbac_50_roles.json` : « Nb rôles » (déjà gatée, bloc §3 count) et — juste à sa gauche — « **Familles rattachées** » : pour chaque portail, l'ENSEMBLE des `famille` distinctes de ses rôles (ex. `direction` → {direction, finance, faisabilite, legal}). Cette 2ᵉ colonne était **NON gatée** : le bloc §3 count la capture avec `[^|]*` (il ne lit QUE le compte), le §5 « résumé » traite volontairement les regroupements comme ÉDITORIAUX (pas de mapping famille↔groupe, cf. memory `rbac-spec-portail-count-gate`), et `check_artifacts` est aveugle (la SPEC n'est pas un `out/*.json`). Même classe que « colonne SET gatée seulement sur son COUNT voisin » (roleprofile per-profil count vs quels rôles). **Le « vert trompeur »** — reclasser la `famille` d'un rôle À L'INTÉRIEUR d'un portail change le SET de familles **sans** changer le « Nb rôles » du portail → la cellule §3 ment en silence pendant que le count-gate reste VERT. Prouvé (M4) : reclasser les DEUX rôles `legal` de `direction` (Directeur Juridique + Agent ONAPI) en `finance` laisse le compte à **9==9 VERT** tandis que le nouveau gate mord **FANTÔME {legal}** (la SPEC liste encore `legal`, le contrat ne l'a plus). Ajouter/retirer une famille (rôle d'une famille inédite) laisse aussi la cellule périmée. Aucune suite `tests/` (FONCTIONS de validation, pas l'ancre SPEC↔contrat) ne mord. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « 1quater-bis » juste après le §3 count-gate, RÉUTILISE `src_roles`/`spec_rd` déjà chargés) — RECOMPUTE le SET de familles distinctes PAR portail depuis `rbac_50_roles.json[roles][].famille` et exige, par ENSEMBLE (absent ET fantôme), l'égalité avec les jetons `famille` de la cellule. On ne gate QUE les jetons `famille` bruts (identifiant de tête de chaque item séparé par `,`, via `(?:^|,)\s*([a-z]+)`), **PAS** les parenthèses ÉDITORIALES (« finance (CFO) », « faisabilite (analyste/rendu/IFC/économiste) » — paraphrase, pas contrat, #6). Ordre NON gaté (réordonner les rôles dans le contrat byte-gaté est sémantiquement neutre pour l'appartenance). Un portail sans cellule échoue AUSSI (INTROUVABLE = régression de traçabilité #6). **Vérif** — 6 checks verts sur l'arbre propre (un par portail). **5 morsures** adversariales : M1 SPEC retire `legal` de `direction` ⇒ MANQUE {legal} · M2 SPEC ajoute `rh` à `achat` ⇒ FANTÔME {rh} · M3 SPEC `finance`→`financ` sur `compta` ⇒ MANQUE {finance} · FANTÔME {financ} · **M4 SILENT-GREEN : contrat reclasse les 2 rôles `legal`→`finance` dans `direction`, compte inchangé (count-gate 9==9 VERT) ⇒ familles-gate mord FANTÔME {legal} — preuve que l'autorité est le CONTRAT, pas la cohérence interne de la SPEC** · M5 SPEC supprime la ligne `direction` ⇒ cellule INTROUVABLE. Gates re-verts : `check_artifacts` · `check_ci_integrity` · `check_docs` · `check_readme_claims` · `check_regression` · `guard_constraints` tous exit 0. --- ## Sprint 8 · buffer · Faisabilité — ancrage du TEMPLATE CANONIQUE v1.0 (5 surfaces #9/#10) sur CLAUDE.md **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « constante canonique CLAUDE.md recopiée à la main dans une surface data-derived » (re-dérive la valeur depuis la contrainte, jamais la cohérence interne) couvre déjà le générateur (`model.py`/README/renderer/scorer/ oracle-test), les fiches ERPNext/CRM (« Fiches #10 »), le module fiscal (`moneda`), la marque SEO §Entités, la persona Chat OTOIA, les tokens branding #4, etc. Un fichier restait NON gaté (grep : absent de tous les `ci/*.sh`) et c'est paradoxalement **le plus normatif** : `05_deliverables_mvp/faisabilite/ TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md`, le DOCUMENT-MODÈLE « CANONIQUE » (Livrable Sprint 1) que **toute faisabilité en production DOIT reproduire**, et que le générateur 4 volets + le Publiciste + la régénération batch S4 sont censés suivre. Ce template recopie **à la main les 6 valeurs de CLAUDE.md #9/#10 à CINQ surfaces structurées**, toutes se réclamant de « contrainte #9 »/« #10 », **aucune gatée** : 1. **l.28** intro §0 « frais **3 %** · marketing **8.5 %** · point d'équilibre **52 %** » (3 pourcentages #9, associés par mot-clé) ; 2. **l.144-146** §5.2 — 3 lignes de table « **N %** (fixe · contrainte #9) » ; 3. **l.29** intro §0 « **USD + DOP** · format **Letter US** · paiements **Cardnet** » ; 4. **l.149-151** §5.2 — 3 lignes « **valeur** (… contrainte #10) » (la ligne paiement citant l'exclusion « ❌ pas-X ») ; 5. **l.196** §7 checklist « 3 % / 8.5 % / 52 % · USD+DOP · Letter US · Cardnet présents » (énumération des 6 marqueurs). **Le « vert trompeur »** — si CLAUDE.md #9 passe **52 %→55 %** (ou #10 renomme le prestataire) et que les modules s'alignent, ce TEMPLATE — la RÉFÉRENCE de toutes les faisabilités — reste PÉRIMÉ en silence, imposant à chaque projet un paramètre qui CONTREDIT le mandat pendant qu'il se réclame « contrainte #9/#10 ». Ni `check_artifacts` (ce n'est pas un `out/*.json`) ni `check_docs`/`guard_constraints` ni aucune suite `tests/` (FONCTIONS, jamais l'ancre à CLAUDE.md) ne mordent. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « TEMPLATE FAISABILITÉ CANONIQUE v1.0 · PARAMÈTRES #9/#10 », juste après « Fiches #10 ») — RÉUTILISE `exp` (les 6 valeurs déjà recomputées des lignes #9/#10 de CLAUDE.md · zéro re-dérivation) + `exp_excl` (prestataire exclu du `(pas X)`). Exige que chacune des 5 surfaces == CLAUDE.md : (1) 3 pcts associés par mot-clé ; (2) SET des 3 lignes « (fixe · contrainte #9) » == {3 %,8.5 %,52 %} (mord aussi ligne ajoutée/retirée) ; (3)/(4) chaque valeur #10 présente en gras dans sa région ancrée + l'exclusion ❌ ; (5) énumération des 6 marqueurs == ensemble EXACT (set-diff absent ET en trop). Une surface INTROUVABLE échoue AUSSI (recensement évaporé = régression #6). **Vérif** — 5 checks verts sur l'arbre propre. **11 morsures** adversariales : M1 intro#9 52→55 · M2 ligne#9 8.5→9 · M3 intro#10 Cardnet→Azul · M4 ligne#10 Letter US→A4 · M5 exclusion ❌ Stripe→PayPal · M6 checklist perd Cardnet · M7 checklist 52→60 · M8 intro#9 supprimée (INTROUVABLE) · M9 checklist « présents »→ « présent » (INTROUVABLE) · **M10 CLAUDE.md #9 52→55 SEUL ⇒ cascade sur 3 régions (1)(2)(5)** · **M11 CLAUDE.md #10 Cardnet→Azul SEUL ⇒ cascade sur 3 régions (3)(4)(5)** — les deux preuves que l'AUTORITÉ est CLAUDE.md, pas la cohérence interne du template. Un piège corrigé en route : le groupe capturé est le NOMBRE nu (sans `%`), donc `_pct()` (qui exige un `%`) renvoyait `None` → `float()` direct. Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. --- ## Sprint 8 · buffer · Fiscal e-CF — ancrage du CONTRAT DE FORMAT e-NCF dans le COMPOSEUR `ncf.py` (CODE) + son ORACLE de test **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le bloc « Fiscal · e-CF DGII · CONTRAT DE FORMAT » existant ancre le format de l'identifiant fiscal DGII (`E + tipoeCF(2) + secuencia(10)` = 13, Ley 32-23) sur le SEUL bloc `e_ncf` byte-gaté de `out/ecf_plan.json` (dérivé de `ecf_spec.json`) et couvre **4 surfaces de PROSE** : README ×3 + fiche `erpnext_backend`. Mais le **composeur d'exécution lui-même** — `ecflib/ncf.py`, le code Frappe qui VALIDE et COMPOSE réellement le numéro e-NCF côté VPS/Compupar — porte **TROIS copies INDÉPENDANTES du format, AUCUNE dérivée de l'artefact**, + un oracle de test qui les bénit, tous **NON gatés** : 1. `ENCF_RE = re.compile(r"^E([0-9]{2})([0-9]{10})$")` (l.26) — la **regex de VALIDATION** (cœur de `is_valid_encf`/`parse_encf`) ; 2. `FORMULE = "E + tipoeCF(2) + secuencia(10)"` (l.27) — la chaîne **publiée par `compose_encf`** (renvoyée au caller VPS) ; 3. docstring l.8 « `"E" + tipoeCF (2 chiffres) + secuencia (10 chiffres) → 13 caracteres` » + commentaire l.25 « Ley 32-23 : E + tipo(2) + secuencia(10) = 13 » (deux proses-en-code) ; 4. `tests/test_ecf_dgii.py:246` — oracle hardcodé `assertEqual(…["formule"], "E + tipoeCF(2) + secuencia(10)")`. **Le « vert trompeur »** — un rango DGII à 12 chiffres (évènement ops réel, cité dans le commentaire du gate existant) régénère `ecf_spec→ecf_plan`, donc les 4 proses gatées SUIVENT. Mais `ENCF_RE` reste `{2}{10}` : `compose_encf` zfill à 10 et `is_valid_encf` **REJETTE** un e-NCF désormais valide → **bug d'exécution du composeur fiscal**, pendant que la suite `tests/` reste **VERTE** (son oracle == `FORMULE` périmée, self-consistante). Ni `check_artifacts` (`ncf.py` n'est pas un `out/*.json`) ni `tests/` (teste les FONCTIONS, pas leur ANCRAGE à l'artefact) ne mordent. Même patron que l'oracle/formateur du Publiciste déjà gaté. **Gate ajouté** (`ci/check_readme_claims.sh`, extension du bloc e-NCF · surfaces 5→9) — RÉUTILISE `prefix/tipo_len/seq_len/total` déjà dérivés du bloc `e_ncf` byte-gaté + le helper `ecf_check` (zéro re-dérivation du format #6). Ancre les 5 surfaces CODE/test au SEUL artefact : (5) `ENCF_RE`, (6) `FORMULE`, (7) docstring, (8) commentaire, (9) oracle de test. Une surface absente échoue AUSSI (INTROUVABLE · anti-évaporation #6). **Vérif** — 5 checks verts sur l'arbre propre. **7 morsures** adversariales : M1 `ENCF_RE {10}→{12}` · M2 `FORMULE secuencia(10)→(12)` · M3 docstring `10→12 chiffres` · M4 commentaire `secuencia(10)→(12)` · M5 oracle test `(10)→(12)` · **M6 SILENT-GREEN — l'ARTEFACT passe à seq=12 pendant que `ncf.py` reste à 10 ⇒ les 5 surfaces mordent TOUTES (preuve que l'autorité est l'artefact byte-gaté, pas la cohérence interne du composeur)** · M7 `FORMULE` renommée ⇒ INTROUVABLE. Les **39 tests unittest** e-CF restent verts (composeur inchangé). Gates re-verts : `check_artifacts` · `check_ci_integrity` · `check_docs` · `check_readme_claims` · `check_regression` · `guard_constraints` tous exit 0. --- ## Sprint 8 · buffer · CRM Commissions — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (7ᵉ instance du ledger) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main) a déjà couvert **Chat OTOIA**, **SEO**, **Fiscal e-CF**, **Legal CONFOTUR**, **CRM Dossier Vente** et **QA Audit 5D**. Le **7ᵉ générateur** — dernier candidat restant recensé au ledger (`crm/commissions` ·10) — était NON gaté : `crm/commissions/commissions_gen.py` (barème de commissions vendeurs · roadmap l.51). Le compte **« 10 invariants »** y est transcrit **à la main à CINQ endroits**, aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un `out/*.json`) : 1. message de succès CLI `validate` « … schéma + 10 invariants verts. » (l.209) ; 2. aide du sous-parseur `validate` « schéma + 10 invariants » (l.221) ; 3. README bloc de commande « schéma + 10 invariants » (l.60) ; 4. README titre de section « ## Les 10 invariants » (l.64) ; 5. README prose de score « 10 invariants de cross-cohérence » (l.92). Ajouter un contrôle `# 11 ·` à `_validate` sans toucher les 5 chaînes ⇒ le README ET les self-reports du `.py` mentent en silence pendant que la CI applique 11 contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ». **Particularité du registre Commissions** (≈ Dossier Vente) : le ledger est **PROPRE 1..10** — le **schéma est compté À PART** (phrasé « schéma + N invariants »), donc pas de schéma-#1 non marqué et pas de split a/b ; on garde la détection de doublon nu comme filet. Le module porte AUSSI un **SECOND nombre** — « **25 tests** » (la suite unittest, déjà gatée par ailleurs) présent dans le README : l'ancre sur le mot « invariants » l'exclut nativement (« 25 tests » ≠ « invariants ») ⇒ aucun faux positif. Comme Dossier Vente, ce README A un titre « ## Les N invariants » ET une énumération numérotée du ledger → on gate (a)(b)(c)(d)(e). **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « CRM · Commissions · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (`# N ·` de `_validate`) et exige : - **(a)** ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; - **(b)** toute mention « N invariants » du `.py` == |ledger| ; - **(c)** l'annonce du titre README « ## Les N invariants » == |ledger| ; - **(d)** toute mention « N invariants » du README == |ledger| ; - **(e)** l'ÉNUMÉRATION numérotée de la section (« 1. … 2. … N. ») contiguë 1..N. Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation). **Vérif** — 5 checks verts sur l'arbre propre (ledger 1..10 = 10 marqueurs `# N ·`, schéma compté à part). **7 morsures** adversariales : M1 self-report `.py` 10→11 (b) · M2 titre README 10→11 (c+d) · M3 prose README 10→11 (d) · **M4 ajout marqueur `# 11 ·` ⇒ ledger→11 pendant que la prose reste 10 (mord `.py` ET README titre/mention ET l'énumération, preuve que l'autorité est le CODE)** · M5 suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 doublon nu `# 5 ·` ⇒ détection de vrai doublon (a) · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e). Le distinct « 25 tests » reste INVISIBLE au gate (ancre « invariants »). Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. **Série ledger close : les 7 générateurs portant le nombre magique d'invariants sont désormais tous gatés.** --- ## Sprint 8 · buffer · QA Audit 5D — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (6ᵉ instance du ledger) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main) a déjà couvert **Chat OTOIA**, **SEO**, **Fiscal e-CF**, **Legal CONFOTUR** et **CRM Dossier Vente**. Le **6ᵉ générateur** portant le même nombre magique restait NON gaté : `qa/audit_5d/audit_5d_gen.py` (candidat restant recensé : `crm/commissions` ·10, ledger propre non gaté). Le compte **« 15 invariants »** y est transcrit **à la main à SIX endroits**, aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un `out/*.json`) : 1. docstring sous-commande module (l.19 « valide schéma + 15 invariants ») ; 2. docstring `_validate` (l.80 « 15 invariants de cohérence audit/contrôles ») ; 3. message de succès CLI `validate` « … 15 invariants — verdict … » (l.218) ; 4. aide du sous-parseur `validate` « valide schéma + 15 invariants » (l.229) ; 5. README bloc de commande « schéma + 15 invariants » (l.66) ; 6. README prose d'architecture « CLI build/validate + **15 invariants** » (l.81). Ajouter un contrôle `# 16 ·` à `_validate` sans toucher les 6 chaînes ⇒ le README ET les self-reports du `.py` mentent en silence pendant que la CI applique 16 contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ». **Particularité du registre Audit 5D** (≈ Dossier Vente, ≠ Fiscal) : le ledger est **PROPRE 1..15** — le **schéma est compté À PART** (phrasé « schéma + N invariants »), donc **pas de schéma-#1 non marqué** et **pas de split a/b** ; on garde la détection de doublon nu comme filet. MAIS le module porte un **SECOND nombre distinct** — « **17 contrôles** » (le compte de fonctions-contrôle de `qalib/controls.py`, déjà gaté par ailleurs), présent dans le `.py` ET le README : un `\d+ invariants` ancré sur le mot « invariants » l'**exclut nativement** (« 17 contrôles » ≠ mot « invariants ») ⇒ **aucun faux positif** sur l'arbre propre. Contrairement à Dossier Vente/Fiscal, ce README n'a **NI titre « ## Les N invariants » NI énumération numérotée** du ledger → on ne gate que (a)(b)(d). **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « QA · Audit 5D · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (`# N ·` de `_validate`) et exige : - **(a)** ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; - **(b)** toute mention « N invariants » du `.py` == |ledger| ; - **(d)** toute mention « N invariants » du README == |ledger|. Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation). **Vérif** — 3 checks verts sur l'arbre propre (ledger 1..15 = 15 marqueurs `# N ·`, schéma compté à part). **6 morsures** adversariales : M1 self-report `.py` 15→16 (b) · M2 mention README 15→16 (d) · **M3 ajout marqueur `# 16 ·` ⇒ ledger→16 pendant que la prose reste 15 (mord `.py` ET README, preuve que l'autorité est le CODE)** · M4 suppression `# 7 ·` ⇒ gap non-contigu (a) · M5 doublon nu `# 5 ·` ⇒ détection de vrai doublon (a) · **M6 no-false-positive** : le distinct « 17 contrôles » reste INVISIBLE au gate (l'ancre « invariants » l'exclut, arbre propre VERT). Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. --- ## Sprint 8 · buffer · CRM Dossier Vente — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (5ᵉ instance du ledger) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main) a déjà couvert **Chat OTOIA**, **SEO**, **Fiscal e-CF** et **Legal CONFOTUR**. Le **5ᵉ générateur** portant le même nombre magique restait NON gaté : `crm/dossier_vente/doctype_dossier_vente_gen.py` (candidats restants recensés : `qa/audit_5d` ·15, `crm/commissions` ·10, tous ledgers propres non gatés). Le compte **« 12 invariants »** y est transcrit **à la main à SEPT endroits**, aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un `out/*.json`) : 1. docstring module (l.14 « schéma + 12 invariants de ») ; 2. docstring `_validate` (l.77 « 12 invariants de cross-cohérence ») ; 3. message de succès CLI `validate` « … schéma + 12 invariants verts. » (l.218) ; 4. aide du sous-parseur `validate` (l.230) ; 5. README bloc de commande « schéma + 12 invariants » (l.56) ; 6. README titre de section « ## Les 12 invariants » (l.60) ; 7. README prose de score « 12 invariants de cross-cohérence » (l.83). Ajouter un contrôle `# 13 ·` à `_validate` sans toucher les 7 chaînes ⇒ le README ET les self-reports du `.py` mentent en silence pendant que la CI applique 13 contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ». **Particularité du registre Dossier Vente** (≈ CONFOTUR, ≠ Fiscal) : le ledger est **PROPRE 1..12** — le **schéma est compté À PART** (phrasé « schéma + N invariants »), donc **pas de schéma-#1 non marqué** et **pas de split a/b**. On garde néanmoins la détection de doublon nu comme filet. Le README porte AUSSI une **énumération numérotée** sous le titre de section (item 1 = conformité au schéma, puis les contrôles), gatée en contiguïté 1..N. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « CRM · Dossier Vente · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (`# N ·` de `_validate`) et exige : - **(a)** ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; - **(b)** toute mention « N invariants » du `.py` == |ledger| ; - **(c)** l'annonce du titre README « ## Les N invariants » == |ledger| ; - **(d)** toute mention « N invariants » du README == |ledger| ; - **(e)** l'ÉNUMÉRATION numérotée de la section (« 1. … 2. … N. ») contiguë 1..N. Une mention ou le registre absent échoue AUSSI (traçabilité #6). **Vérif** — 5 checks verts sur l'arbre propre (ledger 1..12 = 12 marqueurs `# N ·`, schéma compté à part). **7 morsures** adversariales : M1 self-report `.py` 12→13 (b) · M2 titre README 12→13 (c+d) · M3 prose README 12→13 (d) · **M4 ajout marqueur `# 13 ·` ⇒ ledger→13 pendant que la prose reste 12 (mord `.py` ET README ET l'énumération, preuve que l'autorité est le CODE)** · M5 suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 doublon nu `# 5 ·` ⇒ détection de vrai doublon (a) · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e). Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. --- ## Sprint 8 · buffer · Legal CONFOTUR — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (4ᵉ instance du ledger) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main) a déjà couvert **Chat OTOIA**, **SEO** et **Fiscal e-CF**. Le **4ᵉ générateur** portant le même nombre magique restait NON gaté : `legal/confotur/confotur_application_gen.py`. Le compte **« 14 invariants »** y est transcrit **à la main à SIX endroits**, aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un `out/*.json`) : 1. docstring module (l.24 « schéma + 14 invariants de ») ; 2. docstring `_validate` (l.87 « 14 invariants de cross-coherence ») ; 3. message de succès CLI `validate` « … schéma + 14 invariants verts. » (l.283) ; 4. aide du sous-parseur `validate` (l.295) ; 5. README bloc de commande « schéma + 14 invariants » (l.56) ; 6. README commentaire d'arborescence « CLI build/validate · 14 invariants » (l.82). Ajouter un contrôle `# 15 ·` à `_validate` sans toucher les 6 chaînes ⇒ le README ET les self-reports du `.py` mentent en silence pendant que la CI applique 15 contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ». **Particularité du registre CONFOTUR** (≠ Fiscal) : le ledger est **PROPRE 1..14** (pas de schéma-#1 non marqué, pas de split a/b). MAIS le README porte AUSSI un compte de **SOUS-ENSEMBLE éditorial** — « **2 invariants** refusent tout champ monétaire » (l.92) — à NE PAS confondre avec le total. Un `\d+ invariants` naïf mordrait donc l'arbre PROPRE (il verrait `{2, 14}`). Le gate exclut le sous-ensemble par **lookahead négatif** `(\d+)\s+invariants(?!\s+refusent)`, appliqué au `.py` comme au README. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Legal · CONFOTUR · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (`# N ·` de `_validate`) et exige : - **(a)** ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; - **(b)** toute mention-total « N invariants » du `.py` (hors « refusent ») == |ledger| ; - **(c)** toute mention-total du README (hors « refusent ») == |ledger|. Une mention ou le registre absent échoue AUSSI (disparition d'un chiffre = régression de traçabilité #6). **Vérif** — 3 checks verts sur l'arbre propre (ledger 1..14 = 14 marqueurs `# N ·`). **6 morsures** adversariales : M1 self-report `.py` 14→15 (b) · M2 README commande 14→15 (c) · M3 README arborescence 14→13 (c) · **M4 ajout marqueur `# 15 ·` ⇒ ledger→15 pendant que la prose reste 14 (mord `.py` ET README, preuve que l'autorité est le CODE)** · M5 suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 doublon nu `# 5 ·` ⇒ détection de vrai doublon (a). **M7 no-false-positive** : sous-ensemble « 2 → 3 invariants refusent » reste VERT (le lookahead exclut bien le sous-ensemble). Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. --- ## Sprint 8 · buffer · Publiciste — gate d'ANCRAGE des DEVISES du site public sur CLAUDE.md #10 **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le bloc `Publiciste/branding` existant ancre les **4 tokens DESIGN** de CLAUDE.md #4 (2 couleurs + 2 typographies) sur leurs 4 copies. Mais le MÊME module publiciste porte une **2ᵉ constante constitutionnelle** — les **devises CLAUDE.md #10 (« USD + DOP »)** — recopiée à 4 endroits data-derived, **aucun gaté**, tous invisibles à `check_artifacts` (pas un `out/*.json`) : 1. `lib/branding.py` `DEVISE_PRIMAIRE`/`DEVISE_SECONDAIRE` — constantes auto- étiquetées « contrainte #10 · USD + DOP », sans gate ; 2. `lib/generator.py` `_fmt_usd`/`_fmt_dop` — les **formateurs qui IMPRIMENT le code devise sur la grille de prix du site public** `vente.otov7.com` (sortie face-public : une devise hors #10 = mandat trahi en silence) ; 3. `lib/generator.py` footer HTML « Prix … en USD + DOP (Cardnet) » ; 4. `tests/test_publiciste.py` — l'oracle hardcodé `assertIn("USD 150 000", …)`, une copie de plus jamais comparée à CLAUDE.md. Aucune suite `tests/` (qui teste des **fonctions** de rendu, pas l'ancre à CLAUDE.md) n'attrape la dérive : renommer la devise #10 et aligner le module reste vert alors que le site émettrait la mauvaise devise ; ne rien bouger reste vert alors que `branding.py` se dit « #10 » en mentant. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Publiciste/generator+branding · DEVISES CANONIQUES ») — RE-PARSE les 2 devises depuis la ligne #10 de CLAUDE.md (indépendamment de `exp` amont · aucun skip hérité) puis exige : - **(a)** `branding.py` DEVISE_PRIMAIRE/SECONDAIRE == #10 (ordre inclus) ; - **(b)** l'ensemble des codes émis par les formateurs `generator.py` == #10 ; - **(c)** le footer HTML cite les 2 devises dans l'ordre + `(Cardnet)` ; - **(d)** l'oracle du test == devise primaire #10. Un claim absent échoue AUSSI (traçabilité #6 · anti-évaporation). **Vérif** — 5 checks verts sur l'arbre propre. **6 morsures** adversariales : M1 branding primaire USD→EUR · M2 formateur `_fmt_usd`→EUR (hors #10) · M3 footer USD+DOP→USD+EUR · M4 oracle test → EUR · **M5 CLAUDE.md #10 USD→EUR = cascade sur les 4 copies simultanément** (preuve que l'ancre est CLAUDE.md, pas la cohérence interne) · M6 footer supprimé → INTROUVABLE. Gates re-verts : `check_readme_claims` · `check_docs` · `guard_constraints` · `check_ci_integrity` tous exit 0. --- ## Sprint 8 · buffer · Fiscal e-CF DGII : gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »). Le pattern « ledger d'invariants » a déjà été appliqué à **Chat OTOIA** et **SEO** (recompute le nombre d'invariants CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main). Le **3ᵉ générateur** portant le même nombre magique restait NON couvert : `fiscal/ecf_dgii/ecf_dgii_gen.py`. Le compte **« 12 invariants »** y est transcrit **à la main à SEPT endroits**, aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un `out/*.json`) : 1. `ecf_dgii_gen.py` docstring module (l.24) ; 2. docstring `_validate` (l.110) ; 3. message de succès CLI `validate` « … schéma + 12 invariants verts. » (l.277) ; 4. aide du sous-parseur `validate` (l.289) ; 5. README bloc de commande « schéma + 12 invariants » (l.65) ; 6. README titre de section « ## Les 12 invariants » (l.69) ; 7. README prose de score « 12 invariants de cross-cohérence » (l.105). Ajouter un contrôle `# 13 ·` à `_validate` sans toucher les 7 chaînes ⇒ le README ET les self-reports du `.py` mentent en silence pendant que la CI applique 13 contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la prose) n'attrape ce « vert trompeur ». **Particularité du registre fiscal** (≠ SEO/Chat OTOIA au ledger propre 1..N) : l'**invariant 1 est la validation de SCHÉMA** (`validate(bundle, schema)`, **non marquée** `# 1 ·`), les marqueurs couvrent **2..N**, et l'invariant **12 est splitté** en `# 12a ·` (unicité évènements) / `# 12b ·` (comptes du manifeste) — deux volets d'un même invariant. Le gate reconstruit donc le ledger = `{1:schéma}` ∪ marqueurs-top-level, en collapsant 12a/12b, tout en **mordant un vrai doublon** d'un numéro NON suffixé. **Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Fiscal · e-CF DGII · IDENTITÉ du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source (`# N ·` de `_validate`) et exige : - **(a)** ledger CONTIGU 1..N (schéma #1 présent + marqueurs 2..N sans lacune ; doublon nu = défaut mordu ; 12a/12b comptés 1×) ; - **(b)** toute mention « N invariants » du `.py` == |ledger| ; - **(c)** l'annonce du titre README == |ledger| ; - **(d)** toute mention « N invariants » du README == |ledger| ; - **(e)** l'ÉNUMÉRATION numérotée du README (« 1. … 2. … N. ») elle-même contiguë 1..N (item ni sauté ni dupliqué). **Vérif** — 5 checks verts sur l'arbre propre (ledger 1..12 = schéma #1 + 11 marqueurs). **8 morsures** adversariales : M1 self-report `.py` 12→13 (b) · M2 titre README 12→13 (c+d) · M3 mention README 12→13 (d) · M4 ajout marqueur `# 13 ·` ⇒ ledger→13 pendant que la prose reste 12 (b+c+d, preuve que l'autorité est le CODE, pas la prose) · M5 suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 doublon nu `# 5 ·` ⇒ détection de vrai doublon · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e) · M8 suppression `validate(bundle, schema)` ⇒ invariant 1 introuvable. Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · `check_readme_claims` · `check_regression` tous exit 0.