Files
oto-enterprise-os-dtp/05_activity_log/2026-08-02.md
T
Claude Code DTP Worker 215c9e6f25 [DTP-Worker 20260802_060518] Sprint 8 · buffer · Traçabilité : ancrage de la CITATION roadmap_ref VERBATIM (l.N + libellé) des 4 MANIFESTs sur la ligne réelle de la roadmap
Gate anti-dérive #6 : demo/scenarios · frontend/chat_otoia · frontend/portails ·
mobile/app_config publient un roadmap_ref « …l.A[-B] « <bullet> » » dans
out/MANIFEST.json (byte-gaté). check_artifacts ne prouve QUE spec==MANIFEST ;
INV11 n'ancre que les roadmap_line NUMÉRIQUES de qa/acceptance. Reformuler/
renuméroter la roadmap laissait les 4 citations périmées en silence (silent-green
prouvé : reword l.63 ⇒ 6 gates verts, chat_otoia cite toujours l'ancien texte).

check_readme_claims : découvre chaque MANIFEST à citation et exige (1) fichier ==
roadmap canonique, (2) libellé normalisé ⊆ lignes A..B réelles, (3) spec ==
MANIFEST ; 4 modules connus EXIGÉS présents (anti-évaporation). 5 checks verts ·
7 morsures (M1 silent-green reword roadmap · M2 renumber · M3 drift citation ·
M4 wrong file · M5 spec≠manifest · M6 évaporation · M7 hors-plage). Suite complète
exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 06:15:53 +00:00

667 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Activity log · 2026-08-02 · Claude Code DTP Worker
## Sprint 8 · buffer · Traçabilité roadmap — ancrage de la CITATION `roadmap_ref` VERBATIM (n° ligne + libellé) des artefacts sur la ligne RÉELLE de la roadmap
**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention · toujours
vérifier sources archives »). Le pattern « citation vérifiée, pas déclarée » (INV11
de `qa/acceptance` ancre les `roadmap_line` NUMÉRIQUES de son artefact au bullet
DELIVERABLE réel) portait un angle mort : **quatre** modules — `demo/scenarios`,
`frontend/chat_otoia`, `frontend/portails`, `mobile/app_config` — publient dans leur
`out/MANIFEST.json` (byte-gaté) un champ `roadmap_ref` de forme
`« …ROADMAP… · Sprint N l.A[-B] « <libellé bullet> » »`, où le **libellé est une
transcription VERBATIM** du bullet de la roadmap à la ligne citée. Or **aucun gate**
ne vérifiait que la ligne A[-B] porte réellement ce texte : `check_artifacts` ne
prouve QUE `spec==MANIFEST` byte-for-byte (la MÊME citation recopiée des deux côtés,
jamais contredite) ; INV11 ne touche QUE les `roadmap_line` numériques de SON
artefact ; l'ancrage Expo mobile ne teste que « roadmap » + « Expo N » dans
`expo_sdk_source`, pas la citation complète.
**Le « vert trompeur »** — RÉORDONNER la roadmap (les n° de ligne glissent) ou
REFORMULER un bullet laisse ces 4 citations pointer une ligne qui dit AUTRE CHOSE :
un agent lisant le MANIFEST pour retrouver la promesse d'origine tomberait sur le
mauvais bullet. **Prouvé par mutation** : reformuler le bullet l.63 (`Chat OTOIA
embedded…`) laissait les **6 gates VERTS** pendant que `chat_otoia/out/MANIFEST.json`
citait encore « … Chat OTOIA embedded … » à l.63. Aucune suite `tests/` (FONCTIONS
de résolution, jamais l'ancre à la roadmap) ne mord.
**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Traçabilité roadmap », juste
avant `sys.exit`) — DÉCOUVRE tous les `out/MANIFEST.json` (glob 1 & 2 niveaux)
portant un `roadmap_ref` citant `(l.A[-B] «…»)` et exige, par module : **(1)** le
fichier cité == la roadmap canonique ; **(2)** le libellé, NORMALISÉ (markdown
`**`/`- ` retirés · espaces compressés · NFC), est SOUS-CHAÎNE des lignes A..B
réelles (robuste à la troncature du bullet et à la plage multi-lignes `l.56-57`) ;
**(3)** le spec du module porte le MÊME `roadmap_ref` (identité spec↔artefact publié).
Bornes de ligne hors-plage attrapées (renumérotation). Les **4 modules connus** sont
EXIGÉS présents (anti-évaporation #6 : retirer un `roadmap_ref` échoue) ; tout AUTRE
MANIFEST à citation est ancré en bonus (future-proof).
**Vérif** — 5 checks verts sur l'arbre propre (4 citations ancrées + recensement
« 4 modules attendus présents »). **7 morsures** adversariales, gate lancé SEUL,
reverts par `cp` ciblé (jamais `git checkout` large — cf. incident consigné) :
**M1 SILENT-GREEN** — reformuler le bullet l.63 (AUTORITÉ), MANIFEST inchangé ⇒
libellé ABSENT (preuve que l'autorité est la ROADMAP, pas la cohérence interne de
l'artefact) · M2 renuméroter le MANIFEST `l.63→l.62` (ligne existante mais autre
bullet) ⇒ ABSENT · M3 dériver le libellé du MANIFEST lui-même ⇒ ABSENT · M4 citer
un autre fichier roadmap ⇒ fichier ≠ canonique · M5 dériver la citation du SEUL spec
⇒ spec ≠ MANIFEST (artefact incohérent) · M6 retirer `roadmap_ref` du MANIFEST ⇒
ÉVAPORÉE ['frontend/chat_otoia'] · M7 citer `l.9999` ⇒ plage hors roadmap (1..92).
Suite complète re-verte : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0.
## Sprint 8 · buffer · RBAC SPEC §6 — ancrage de l'ÉNUMÉRATION des DocTypes `custom` sur le catalogue du contrat (2ᵉ surface prose)
**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention »). Le
générateur `rbac_fixtures_gen` émet `MANIFEST.custom_doctypes_a_confirmer` =
l'ensemble des DocTypes `custom: true` du contrat `rbac_50_roles.json` (les DocTypes
DTP **à créer** côté VPS avant import — `Faisabilité`, `Publiciste Log`, `CONFOTUR
Application`, `API Access`). Cet ensemble data-derived est transcrit À LA MAIN à
**deux** surfaces prose : (1) `rbac/fixtures_gen/README.md`**déjà gatée** (bloc
« RBAC fixtures · catalogue custom ») ; (2) `RBAC_50_ROLES_SPEC.md §6` l.117
**DocTypes `custom: true`** (`Faisabilité`, `Publiciste Log`, `CONFOTUR
Application`, `API Access`) ») — **NON gatée**. Le bloc `apply_plan` ne gate que le
COMPTE (`len(custom_doctypes)`), aveugle aux NOMS ; `check_artifacts` est aveugle
(la SPEC n'est pas un `out/*.json`).
**Le « vert trompeur »** — AJOUTER un DocType `custom` au contrat (le MANIFEST
rebâtit l'entrée) ou RENOMMER/ÉCHANGER l'un des 4 laisse §6 à ses 4 noms périmés en
silence : la cible d'application VPS énumérée par la SPEC ne correspond plus aux
DocTypes que l'agent ERPNext doit réellement créer. Vérifié par mutation : dropper
`Publiciste Log` de §6 laissait le gate à **exit 0**. Même patron que la 2ᵉ surface
CONFOTUR/entités (README gaté, fiche non).
**Gate ajouté** (`ci/check_readme_claims.sh`, extension du bloc « RBAC fixtures »,
point 3) — RÉUTILISE `fx_cdt` (= `MANIFEST.custom_doctypes_a_confirmer`, byte-gaté,
déjà dérivé par le point 1) et le normaliseur d'accents `_fx_acc` (zéro duplication
du contrat) : capte l'énumération §6 (`DocTypes `custom: true`** (…)` sur le texte
whitespace-normalisé, robuste au retour-ligne 117↔118), extrait les tokens
backtickés, et exige set-diff EXACT == catalogue MANIFEST (absent ET en trop, casse/
accents neutralisés). Énumération INTROUVABLE = échec (anti-évaporation #6).
**Vérif** — 1 check vert sur l'arbre propre (les 4 == MANIFEST). **5 morsures**
adversariales : A dropper `Publiciste Log` ⇒ absent · B renommer `API Access``API
Acces` ⇒ absent+en-trop · C ajouter `Ghost DocType` fantôme ⇒ en-trop · **D
SILENT-GREEN : le MANIFEST GAGNE un 5ᵉ DocType custom (§6 inchangé) ⇒ mord « absents=
[new custom dt] » — preuve que l'autorité est l'ARTEFACT, pas la cohérence interne
de la SPEC** · E énumération §6 supprimée ⇒ INTROUVABLE. Reverts par chemin ciblé
(jamais `git checkout` large — cf. incident consigné). Suite complète re-verte :
`check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` ·
`check_regression` · `check_readme_claims` tous exit 0.
**Note process** — un candidat initial (ancrage des attributs de rôle des fiches
`mobile`/`rendu`/`ifc_speckle`) s'est révélé DÉJÀ couvert par les blocs existants
« Fiche Mobile » / « Fiches Faisabilité » : la ligne d'index `MEMORY.md` était
périmée vs le corps détaillé de la mémoire (classe close le 2026-08-01). Doublon
écarté (interdit #5 « jamais accumuler doublons »), index mémoire corrigé.
---
## Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'ÉNUMÉRATION des `scope_donnees` (ligne Row-level) sur l'ENUM du schéma
**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). La table §2 « Modèle RBAC » de
`05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` ouvre par « trois niveaux, **tous
couverts par le schéma** » puis sa 3ᵉ ligne (« **Row-level** (User Permission) |
`scope_donnees` | … ») ÉNUMÈRE à la main l'ensemble EXHAUSTIF des portées
row-level : `own` · `equipe` · `entite` (Company) · `groupe` (consolidé). Par sa
phrase d'intro, cet ensemble EST l'`enum` `scope_donnees` de `rbac.schema.json`
(la DÉFINITION du modèle). Or **aucun gate ne le touchait** : §3 compte les rôles
par portail, §4 les entités, §5 le résumé, et le bloc `userperm` gate la table
`scope → mécanisme` d'un **autre** artefact (`userperm_gen/out`) — tous aveugles à
l'énumération du §2. Même classe que §4 (colonne ancrée sur une source-de-vérité
structurée), côté schéma cette fois.
**Le « vert trompeur »** — si le schéma AJOUTE une portée (`region` pour un besoin
ops réel) ou en RETIRE une (`own` supprimé, schéma resserré), le §2 reste PÉRIMÉ en
silence : il documente un modèle qui CONTREDIT le schéma tout en se réclamant
« couvert par le schéma ». Ni `check_artifacts` (la SPEC n'est pas un `out/*.json`)
ni `tests/` (FONCTIONS de validation, jamais l'ancre SPEC↔schéma) ne mordent.
**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « 1octies », juste après le §5)
— charge l'`enum` `scope_donnees` depuis `rbac.schema.json` (source de vérité du
modèle) puis exige : **(a)** ENSEMBLE des jetons `scope` backtickés de la ligne §2
== enum (set-diff absent ET fantôme) ; **(b)** ANCRAGE — la colonne du milieu NOMME
bien `scope_donnees` (sinon on gate la mauvaise ligne) ; **(c)** sanity — toute
portée RÉELLEMENT exercée par le contrat ∈ enum (une portée en usage jamais dans
l'enum = schéma/contrat incohérents). Les gloses parenthétiques `(Company)`/
`(consolidé)` NE sont PAS gatées (éditoriales · #6). Ligne/section/enum absents =
échec (traçabilité #6).
**Vérif** — 3 checks verts sur l'arbre propre (4 portées `own/equipe/entite/groupe`
== enum). **6 morsures** adversariales : M1 §2 retire `own` ⇒ manquant (a) · M2 §2
ajoute fantôme `region` ⇒ fantôme (a) · **M3 SILENT-GREEN : le schéma GAGNE `region`,
§2 inchangé ⇒ (a) mord `manquant region` — preuve que l'autorité est le SCHÉMA, pas
la cohérence interne de la SPEC** · M4 colonne du milieu renommée `scope_donnees`
`scope` ⇒ ancre perdue (b) · M5 ligne Row-level supprimée ⇒ INTROUVABLE · M6 le
schéma RETIRE `own` (encore exercé par 2 rôles) ⇒ (a) fantôme + (c) portée hors
enum. Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0.
---
## 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.