# Activity log · 2026-08-02 · Claude Code DTP Worker

## Session `20260802_193605` · Sprint 8 · buffer · Faux-positif d'audit récurrent documenté en garde-fou in-repo (`ci/README.md`) : `michel/otov7-platform` (CLAUDE.md #2 · dépôt PRODUIT) ≠ `oto-enterprise-os-dtp` (dépôt MANDAT) → prévention d'une « correction » nuisible de la constitution (PAS un gate #5, PAS un module)

**Nature — verrouiller une non-dérive contre une future « correction » nuisible, pas empiler un gate.**
Ouverture : arbre `git status --porcelain` **vide**, `run_ci.sh` **30 PASS / 0 FAIL / 0 SKIP**
(7 gates `ci/*.sh` + 23 suites module). Recompute indépendant des agrégats du README racine,
concordant : **22/22** qualité (scores = `{100}`) · **564/564** régression · **15/15** recette
(verdict `true`) · **13** agents · `gate.needs` = **30** — **zéro dérive numérique**.

**Audit doc-vs-réalité (fan-out d'exploration).** Balayage des `*.md` de tête (README, CLAUDE.md,
AGENTS_EXISTING_ASSETS, PORTAIL_BANCABLES), `02_master_prompt/`, `03_agents/*/AGENT.md`,
`05_deliverables_mvp/*/README.md`, `ci/README.md`, `04_roadmap/`. **Une seule** candidate
remontée : CLAUDE.md ligne 16 (#2) nomme `michel/otov7-platform` là où la section *Git* (l.62)
et le README racine nomment `michel/oto-enterprise-os-dtp` — apparence de coquille contradictoire.

**Verdict : FAUX POSITIF connu** (mémoire *otov7-platform-not-drift*). Ce sont **deux dépôts
Gitea distincts, tous deux corrects** : `otov7-platform` = dépôt du **produit** (plateforme OTOV7
en construction, cible du refactoring) ; `oto-enterprise-os-dtp` = dépôt de **mandat** (ce worker).
La contrainte #2 verrouille le produit ; la section *Git* précise où vit le mandat. « Aligner »
#2 sur le nom du mandat **corromprait la constitution**. L'agent d'exploration (sans accès à ma
mémoire) l'a re-remonté cette session — preuve que le garde-fou méritait d'être **in-repo**, pas
seulement dans ma mémoire privée.

**Action — garde-fou documenté, à la manière de la « Non-surface documentée » existante.**
Ajout d'un paragraphe **« Non-dérive documentée »** dans la section `guard_constraints.sh` de
`ci/README.md` (propriétaire de la contrainte #2), calqué sur la note `roles_couverts` (« 44 »
non cross-gaté). Il explicite : produit ≠ mandat, ne pas « corriger » CLAUDE.md #2, et pourquoi
il n'y a **rien à gater** (le garde ne vérifie que l'**absence** de plateformes interdites
github/gitlab/bitbucket, pas l'identité d'un repo Gitea autorisé). **CLAUDE.md non touché.**

**Discipline (#5/#6).** **Zéro nouveau module · zéro gate ajouté** — pure documentation d'une
non-dérive, dans le fichier qui héberge déjà ce type de garde-fou. **CLAUDE.md intact** (on ne
« corrige » pas la constitution). Le nouveau texte cite github/gitlab/bitbucket mais chaque
occurrence est sur une ligne portant un marqueur de prohibition (« interdites ») → **ignorée**
par `guard_constraints` (vérifié vert). `check_docs` (liens) + `check_readme_claims` verts.

**Clôture.** `run_ci.sh --static` → **7 PASS / 0 FAIL / 0 SKIP**. Aucune commande touchant au
VPS (`153.75.250.214` · #8). Prochaine tâche : buffer Sprint 8 toujours en **veille saturée**
(fiches 13/13 closes, gates saturés) — ne durcir un gate que si un blind-spot est **prouvé par
mutation-test** (*verify-uncovered-before-gating*), jamais un gate redondant (#5).

## Session `20260802_183604` · Sprint 8 · buffer · Dérive doc-vs-réalité (README racine) : la ligne « Régression » affirmait `regression_run.json` « non commité par design » — FAUX depuis la refonte baseline byte-gatée (`76fb0e7`) → correction de vérité (PAS un gate #5, PAS un chiffre inventé #6)

**Nature — dérive réelle non gatée, corrigée par la vérité (pas un gate de plus).**
Ouverture : arbre propre, `run_ci.sh` **30 PASS / 0 FAIL / 0 SKIP** (7 gates statiques +
23 suites), campagne de gates **saturée**. La file de fiches est close (13/13) et un
**audit doc-vs-réalité frais** (fan-out d'exploration sur les 13 `03_agents/*/AGENT.md`
et les README de module) n'a remonté **aucune** dérive de fiche. Plutôt que d'empiler un
gate redondant (#5, explicitement déconseillé par la mémoire *verify-uncovered-before-gating*)
ou de ré-adosser le `daily_report` (canal 2 déjà à jour — le dernier commit `3ff51d9` **était**
ce rapport), j'ai poussé l'audit vers une surface non couverte : le **README racine**, point
d'entrée du mandat.

**Constat — un fait de commit-status périmé.** `README.md:44-46` (section « État courant »,
puce **Régression**) déclarait : *« le compte de tests agrégé faisant autorité est produit
par `qa/regression run` (**non commité par design**) — plan `…/regression_plan.json` »*.
Or c'est **faux depuis la refonte** :
- Le README **du module** régression (`05_deliverables_mvp/qa/regression/README.md:50,57-61`)
  énonce sans ambiguïté que `out/regression_run.json` est **COMMITÉ** et sert de **baseline
  byte-identique** au gate `ci/check_regression.sh`, qui régénère un `run` frais et exige
  l'**identité byte-for-byte** (+ verdict `PASS`) — mécanisme introduit par le commit
  **`76fb0e7`** (« *fix gate check_regression … run baseline commité* »).
- `git ls-files 05_deliverables_mvp/qa/regression/out/` confirme les **trois** artefacts
  suivis, dont `regression_run.json`.
- Le **commentaire d'en-tête du gate lui-même** (`ci/check_readme_claims.sh:264,269`) qualifie
  `regression_run.json` de « **commité · déterministe** » et de « **compte agrégé faisant
  autorité** ».
Le README racine reflétait donc l'**ANCIEN design** (pré-`76fb0e7`, run non commité) — dérive
classique « la doc de tête retarde d'une refonte ».

**Pourquoi aucun gate ne l'attrapait (angle mort, PAS à re-gater).** `check_readme_claims.sh`
ancre la partie **numérique** de cette puce — le motif `\*\*(\d+) suites gated\*\*` recompté
depuis `regression_plan.json::totals.suites` (=22) — mais le **fait en clair** « (non commité
par design) » est une affirmation de **statut de commit**, hors du périmètre des recomputes de
compteurs. `check_docs.sh` ne valide que les **cibles de liens** (le lien `regression_plan.json`
était valide). La dérive vivait précisément dans l'interstice entre « chiffre gaté » et « lien
valide ».

**2 solutions (workflow #4) — la bonne = corriger la doc, pas gater le statut.**
- ❌ Ajouter un gate « statut de commit de `regression_run.json` » : redondant (#5) — le fait
  « commité » est déjà **structurellement** prouvé par `check_regression` (qui EXIGE le fichier
  commité pour comparer byte-for-byte ; sans lui le gate échouerait). Un checker de prose serait
  un doublon de la vérité déjà enforced.
- ✅ **Corriger la puce vers la réalité.** Nouvelle rédaction (`README.md:44-48`) :
  *« Le **compte de tests agrégé faisant autorité** vit dans `…/regression_run.json`, artefact
  **commité et byte-gaté** : `ci/check_regression.sh` régénère un `run` frais et exige l'identité
  **byte-for-byte** avec la baseline commitée (verdict `PASS`). Recensement déterministe des
  suites : `…/regression_plan.json`. »* — le token gaté **`**22 suites gated**`** est **conservé
  verbatim** (le gate le recompte toujours), le lien `regression_plan.json` est **préservé**, un
  lien vers `regression_run.json` (commité) est **ajouté**, et le faux « non commité par design »
  est **supprimé**. **Aucun chiffre nouveau saisi** (#6) : je n'ai PAS hardcodé le total « 564 »
  dans une surface non gatée — je désigne l'artefact commité comme source, fidèle à la promesse
  du README (« ce README n'introduit aucune donnée nouvelle »).

**Vérif** (en-repo, zéro contact VPS). `run_ci.sh --static` → **7 PASS / 0 FAIL** :
`check_docs` résout le nouveau lien `regression_run.json` (fichier commité) · `check_readme_claims`
retrouve toujours `**22 suites gated**` (== `regression_plan.totals.suites`) · `guard_constraints`
vert. `run_ci.sh` complet → **30 PASS / 0 FAIL / 0 SKIP**. `git status --porcelain` = seuls
`README.md` (+ ce log) modifiés. **Zéro gate ajouté, zéro chiffre inventé, zéro fichier de code
touché, aucune commande VPS.**

## Session `20260802_180604` · Sprint 8 · buffer · Reporting stakeholder (canal 2) — rattrapage du snapshot `daily_reports/2026-08-02` sur les 3 sessions de retouches doc/outillage depuis la clôture `160601` (4ᵉ addendum sourcé · ZÉRO gate ajouté #5)

**Nature — réconciliation de reporting, PAS un gate de plus (CLAUDE.md #5).** Ouverture :
arbre propre, `run_ci.sh` **30 PASS / 0 FAIL / 0 SKIP** (7 gates statiques + 23 suites
module), file substantielle saturée. Un **audit doc-vs-réalité relancé** cette session
(fan-out d'exploration sur les 13 fiches `03_agents/*/AGENT.md`, les README de module
`05_deliverables_mvp/`, et leurs références de chemin internes) n'a remonté **aucune
dérive** : liens Markdown valides, chemins existants, capacités décrites conformes au code.
La campagne de gates reste **saturée** (mémoire *verify-uncovered-before-gating* : ajouter
un gate N+1 sans blind-spot prouvé serait redondant #5). La seule tâche à valeur
non-redondante restante est celle que le `daily_report` lui-même désigne en **« Prochaine
tâche » #1** : tenir le **canal 2** (snapshot stakeholder) à jour à chaque session buffer.

**Constat de staleness.** Le dernier addendum du `daily_report` datait de la clôture
`160601` (commit `4f390b4`). **Trois** sessions ont commité **après**, absentes de
l'instantané direction : `59eb4dc` (`163601`, fiche `devops/AGENT.md` documente
`run_ci.sh`) · `68f8c1a` (`170601`, `run_ci.sh` `-h|--help` + en-tête `--static`) ·
`d2ab269` (`173603`, `ci/README.md` retrait des comptes figés `7 gates / 23 suites`).
Les trois sont des **retouches de doc/outillage** (aucun nouveau module, aucun gate) dans
la lignée directe de `run_ci.sh` — leur trace fine existe au canal 1 (ce journal) mais
aucune synthèse consolidée sourcée côté stakeholder.

**Réalisé — 4ᵉ addendum ADDITIF** (corps `110539` + 3 addenda `120544`/`143555`/`160601`
laissés intacts — record historique préservé) consolidant les 3 sessions. Chaque chiffre
**re-vérifié par recompute `python3` indépendant sur l'artefact commité** (#6) :
- Qualité 4Big **22/22 modules** (scores distincts = {100} · seuil 95 · verdict `PASS`)
  ← `qa/audit_4big/out/quality_report.json` ;
- Régression **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** (`PASS`)
  ← `qa/regression/out/regression_run.json::totals` ;
- Recette **15/15** promesses `status=in_repo` (verdict `true`)
  ← `qa/acceptance/out/acceptance_matrix.json::matrix` ;
- Fiches **13/13** ← `03_agents/*/AGENT.md` ; Câblage CI `gate.needs` = **30 jobs**
  (7 gates `ci/*.sh` + 23 suites · dérivé par `run_ci.sh --list`). État **inchangé, vert**.

**Vérif** — les gates touchant la prose/les liens restent **exit 0** APRÈS l'édition
(l'addendum ajoute des liens Markdown internes que `check_docs` valide) : `check_docs` ·
`check_readme_claims` · `guard_constraints` tous verts. `git status --porcelain` = seul le
`daily_report` (+ ce log) modifié. **Zéro gate ajouté, zéro chiffre inventé, zéro fichier
de code touché, aucune commande VPS.** Le canal 2 est de nouveau à jour (4 addenda ·
7 sessions couvertes).

## Session `20260802_173603` · Sprint 8 · buffer · DevOps doc — `ci/README.md` : retrait des comptes CI figés `7 gates / 23 suites` (même fix que `dcb463e`, un cran plus haut · anti-invention #6, PAS un gate #5)

**Contexte — le décompte figé oublié un niveau au-dessus du lanceur.** Ouverture :
arbre propre, `run_ci.sh` **30 PASS / 0 FAIL** (7 gates statiques + 23 suites de
module), file substantielle saturée (22/22 qualité à 100 · 22 suites / 564 méthodes ·
recette 15/15 `in_repo` · 13 fiches · RBAC 50) — tous re-vérifiés indépendamment en
`python3`. Le commit `dcb463e` avait **dérivé** le décompte `30/23/7` dans `run_ci.sh`
(retrait des comptes figés du **commentaire** du script) précisément parce qu'ils se
périment en silence quand un module est ajouté/retiré à `ci.yml`. Mais la **doc du
lanceur**, `ci/README.md` §« Rejouer le gate EN LOCAL », re-transcrivait toujours à la
main les MÊMES nombres : bloc d'usage L34-35 « `tout gate.needs (7 gates statiques + 23
suites de module)` » / « `seulement les 7 gates ci/*.sh` », et checklist L1601 « les **7
gates statiques** apparaissent ». Même « vert trompeur », un cran au-dessus de l'endroit
déjà corrigé.

**Vérif d'exposition (mémoire *verify-uncovered-before-gating*).** Mutation-test :
`23→24` + `7→8` sur ces deux lignes → `check_readme_claims.sh` **exit 0** et
`check_docs.sh` **exit 0** (les deux VERTS) — surface **réellement non gatée**, aucune
protection existante ne mord. Dérivation indépendante de `gate.needs` de `ci.yml`
(classification `bash ci/` == statique, celle de `run_ci.sh::is_static`) confirme
`30 = 7 gates + 23 suites`, EXACTEMENT ce que le README figeait.

**Décision — FIXER, pas gater** (mémoire *fix-vs-gate-a-transitively-protected-constant*
+ *local-ci-runner*). `ci/README.md:51-54` déclare lui-même que `run_ci.sh` « n'a pas à
être re-gaté (#5) » car son exactitude est **structurelle** ; ajouter un gate N+1 sur la
prose qui DÉCRIT ce lanceur trahirait cette posture et prendrait la mauvaise autorité.
La réponse cohérente est celle que `dcb463e` a déjà appliquée un cran plus bas :
**retirer les nombres figés**, laisser `run_ci.sh` seul propriétaire du décompte (dérivé
de `gate.needs`, imprimé à l'exécution + `--list`).

**Réalisé.**
- `ci/README.md` bloc d'usage L34-37 : « 7 gates statiques + 23 suites » → « gates
  statiques `ci/*.sh` + suites de module » (qualitatif, non chiffré) ; `--list` annoté
  « + décompte dérivé ».
- Ajout d'un paragraphe : le **décompte exact `N jobs = G gates + S suites` est dérivé
  de `gate.needs` et imprimé à l'exécution / `--list`, jamais figé ici** (même raison #6
  qui l'a retiré du commentaire de `run_ci.sh`).
- Checklist §5 L1601 : « les **7 gates statiques** » → « les gates statiques `ci/*.sh` ».
- **Intactes** : les mentions « 7 gates re-verts » du **changelog** (§ sessions de
  mutation-test, L1387-1542) — narratif historique d'états passés VRAIS, pas une
  affirmation de composition VIVE ; les figer serait réécrire l'histoire.

**Vérifs (en-repo, zéro contact VPS).** `run_ci.sh --list` imprime toujours «`30 jobs …
: 7 gates statiques ci/*.sh + 23 suites de module`». `run_ci.sh` complet : **30 PASS · 0
FAIL · 0 SKIP**. `grep` des surfaces vives figées → **aucune restante**. Zéro module,
zéro gate ajouté (#5), zéro chiffre inventé (#6).

## Session `20260802_153601` · Sprint 8 · buffer · DevOps outillage — `run_ci.sh` : décompte `30/23/7` DÉRIVÉ à l'exécution (retrait des comptes figés du commentaire · anti-invention #6, PAS un gate #5)

**Contexte — dernière surface figée dans un outil « anti-dérive par construction ».**
Ouverture : arbre propre, **7 gates verts**, **30/30 jobs `gate.needs`** (`run_ci.sh`).
La file substantielle reste **saturée** (22/22 qualité · 564/564 tests · recette
15/15 · 13 fiches). Balayage des comptes numériques encore susceptibles de dériver :
`run_ci.sh` (créé la session précédente, commit `49123ec`) DÉRIVE dynamiquement la
liste des checks de `gate.needs` (« zéro liste en dur ») — mais son **commentaire
d'en-tête** re-transcrivait à la main « **30 jobs · 23 suites · 7 gates statiques** ».
Or ce sont exactement les quantités que le script calcule déjà : si un job module
est ajouté à `ci.yml`, le script s'adapte tout seul (il exécuterait 31 jobs) tandis
que son commentaire continuerait d'affirmer « 30 · 23 » — un **« vert trompeur »**
de la même classe que celle que l'outil combat, logé dans sa propre doc d'intention.

**Décision — FIXER (éliminer le nombre figé), pas gater** (mémoire *fix-vs-gate-a-
transitively-protected-constant*). Un gate N+1 sur un commentaire de lanceur de
commodité (root ∉ `ci/*.sh`) serait redondant (#5) et prendrait la mauvaise autorité.
La bonne réponse : **rendre le décompte inderivable-à-la-main** — le script le calcule
et l'imprime, le commentaire ne l'affirme plus.

**Livraison — `run_ci.sh` (+20/−4)** :
1. **Ventilation dérivée** des mêmes `ROWS` que l'exécution : `n_gates` (cmd `bash
   ci/*`) vs `n_suites` (le reste avec commande) → `n_jobs`. Ajouter un job à
   `gate.needs` le reclasse ici automatiquement, zéro édition.
2. **En-tête d'exécution** : nouvelle ligne `gate.needs dérivé : 30 jobs = 7 gates
   ci/*.sh + 23 suites de module` (toujours exacte, imprimée par `all` et `--static`).
3. **Pied de `--list`** : `-- 30 jobs dérivés de gate.needs : 7 gates statiques
   ci/*.sh + 23 suites de module.`
4. **Commentaire d'en-tête reformulé** : retrait des littéraux « 30 / 23 / 7 »
   (remplacés par la description QUALITATIVE `bash ci/*.sh` / `python3 -m unittest`
   — le point étant « stdlib pur, aucun pip », pas le nombre) + note explicite « le
   décompte exact est DÉRIVÉ et imprimé à l'exécution, jamais figé ici (#6) ». Ligne
   d'usage `--static` : « les gates statiques » (sans « 7 »).

**Vérif** — `--list` → pied `30 = 7 + 23` ; `--static` → en-tête `30 = 7 + 23` +
**7 PASS / 0 FAIL** ; `run_ci.sh` complet → **30 PASS / 0 FAIL / 0 SKIP**. Les **7
gates re-joués → exit 0** (guard_constraints · validate_json · check_docs ·
check_artifacts · check_regression · check_ci_integrity · check_readme_claims).
Aucun gate ne scanne `run_ci.sh` (surface hors périmètre des gates, cf. son en-tête).
Aucune commande touchant au VPS.

---

## Sprint 8 · buffer · DevOps outillage — `run_ci.sh` : miroir LOCAL du gate CI, dérivé de `gate.needs` (zéro liste en dur · PAS un gate #5)

**Contexte — friction récurrente, pas une surface à gater.** Ouverture : arbre
propre, **7 gates statiques verts**, **30/30 jobs `gate.needs` verts** (7 gates
`ci/*.sh` + 23 suites de module · 564/564 tests), campagne de gates **saturée**
(mémoires *verify-uncovered* / *guard-flags-usage-not-mention* : les surfaces
restantes sont mono-sourcées ou déjà mordues). Ajouter un gate N+1 serait
redondant (#5). La vraie friction non-adressée est **opérationnelle** : chaque
session rejouait à la main une boucle ad-hoc `for g in guard_constraints … ; do
bash ci/$g.sh` (cf. logs des sessions précédentes « 7 gates re-joués »). Une
telle liste écrite à la main **dérive en silence** de ce que la CI enforce (un
gate ajouté/retiré dans `ci.yml` n'y est jamais répercuté) — même classe de
« vert trompeur » que les gates traquent, appliquée au **lanceur** lui-même.

**Livraison — `run_ci.sh` à la racine du dépôt.** Un lanceur de commodité qui
reproduit EN LOCAL, en une commande, l'intégralité du gate de merge :
- `bash run_ci.sh` → tout `gate.needs` (30 jobs) · `--static` → les 7 gates
  `ci/*.sh` seuls (rapide) · `--list` → plan sans exécution.
- **Aucune liste en dur** : le script PARSE `gate.needs` de
  `.gitea/workflows/ci.yml` (la source de vérité que `check_ci_integrity`
  verrouille déjà) et exécute, par job, la commande `run:` déclarée dans son
  `working-directory:`. Ajouter/retirer un gate en CI se répercute
  **automatiquement**, zéro édition — anti-dérive par construction.
- Par périmètre `gate.needs`, exclut à dessein les jobs manuels
  `workflow_dispatch` (`e2e-baseline`/`npm test`, qui exige Node) et le job
  `gate`. → stdlib pur (bash/awk/git/python3), zéro pip, **zéro commande VPS**.
- Code de sortie **0 ssi tous les jobs passent** (même sémantique que le gate) ;
  sur échec, dump des 12 dernières lignes du log fautif + liste des jobs rouges.

**Ce n'est PAS un gate** (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) :
lanceur hors du glob `ci/*.sh`, donc **hors [INV-B]** de `check_ci_integrity` ;
son exactitude est **structurelle** (exécute ce que ci.yml déclare) et n'a pas à
être re-gatée (#5). Placé à la racine (pas dans `ci/`) précisément pour ne pas
déclencher INV-B qui exigerait de le câbler comme job.

**Robustesse prouvée.** Parseur awk **anti-commentaire** (un `working-directory:`
ou `bash ci/…` cité en COMMENTAIRE de ci.yml était capturé par une 1ʳᵉ version →
corrigé par `sub(/^[[:space:]]*#.*$/,"")`, cas réel sur le bloc `check-regression`).
Tests : `--list` = 30 jobs dans l'ordre de `gate.needs` (e2e-baseline/gate
correctement absents) ; `run_ci.sh` = **30 PASS / 0 FAIL** ; **chemin d'échec
vérifié** sur un dépôt-jouet synthétique (`bad-job` → FAIL rendu + dump `boom
detail` + **EXIT=1**) ; hors checkout git → EXIT 3 honnête.

**Doc.** Ligne de nav `README.md` + section « Rejouer le gate EN LOCAL » dans
`ci/README.md` (usage + garantie anti-dérive + rappel « pas un gate #5 »).
Chemins en backticks/liens Markdown vers cibles existantes (`check_docs` vert).

**Vérif finale.** `bash run_ci.sh --static` avec les nouveaux fichiers stagés →
**7 PASS / 0 FAIL** (guard_constraints · validate_json · check_docs ·
check_artifacts · check_regression · check_ci_integrity · check_readme_claims).
Aucun gate perturbé par l'ajout (root ∉ `ci/*.sh` ; nav sans chiffre ; liens
valides). Aucune commande touchant le VPS.

## Sprint 8 · buffer · Reporting stakeholder (canal 2) — rattrapage du snapshot `daily_reports/2026-08-02` sur les 4 sessions buffer intercalées depuis la clôture `120544` (2ᵉ addendum sourcé · ZÉRO gate ajouté #5)

**Nature — réconciliation de reporting, PAS un gate de plus (CLAUDE.md #5).**
Ouverture de session : arbre propre, **7 gates verts**, fiche-audit **close** (13/13),
campagne de gates **saturée** (dernier commit `bd3572b` = audit d'efficacité 7/7 ROUGE).
Aucune surface non couverte évidente ; ajouter un gate serait redondant (#5). La tâche
à VRAIE valeur non-redondante restante est celle que le `daily_report` lui-même désignait
en **« Prochaine tâche » #1** (l.84) : **tenir le canal 2 (snapshot stakeholder) à jour
à chaque session buffer**. Il était **périmé de 4 sessions** — le dernier addendum datait
de la clôture `120544` (commit `2e71c5f`), mais `cb8d931` · `8dcedb0` · `576e78b` ·
`bd3572b` ont commité **après**, absents de l'instantané direction (leur trace fine
existe au **canal 1**, ce journal, mais aucune synthèse consolidée sourcée).

**Faux positif écarté avant action (mémoire `verify-uncovered-before-gating`).**
Un agent d'exploration a remonté `CLAUDE.md:16` (« repo `michel/otov7-platform` ») comme
dérive vs `CLAUDE.md:62` (« Repo mandat DTP : `michel/oto-enterprise-os-dtp` »). **Non
retenu** : `otov7-platform`/`otov7_platform` est une entité RÉELLE et DISTINCTE — l'app
ERPNext plateforme OTOV7 (`AGENTS_EXISTING_ASSETS.md:13` cite `/opt/oto/claude_code_work/
otov7-platform/` comme chemin réel · roadmap S1 `otov7_platform` crm/api). La #2 nomme le
repo **plateforme produit** phare ; le pied de page nomme le repo **mandat** — deux repos
sur la même Gitea, pas une contradiction. **Éditer la constitution `CLAUDE.md` sur une
inférence aurait risqué une invention (#6)** → écarté, aucune modification de `CLAUDE.md`.

**Réalisé — 2ᵉ addendum ADDITIF** (corps `110539` + addendum `120544` laissés intacts —
record historique préservé, comme le 1ᵉ addendum) consolidant les 4 sessions, chaque
chiffre **re-vérifié par recompute `python3` indépendant sur l'artefact commité** (#6) :
- Qualité 4Big **22/22 modules** (scores distincts = {100} · seuil `pass_score` 95 ·
  verdict `PASS`) ← `qa/audit_4big/out/quality_report.json` ;
- Régression **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur** (verdict `PASS`)
  ← `qa/regression/out/regression_run.json` ;
- Recette **15/15** promesses `status=in_repo` (verdict `true`)
  ← `qa/acceptance/out/acceptance_matrix.json` ;
- Fiches **13/13** ← `03_agents/*/` ; Câblage CI `gate.needs` = **30 jobs**, **1** job
  manuel (`e2e-baseline`) ← `.gitea/workflows/ci.yml` (chiffre nouvellement sourcé, tiré
  de l'audit d'efficacité `bd3572b`). État **inchangé et vert**.

**Vérif** — les **7 gates CI** restent **exit 0** APRÈS l'édition (le rapport ajoute des
liens Markdown internes que `check_docs` valide → tous résolus) : `check_docs` ·
`check_readme_claims` · `check_artifacts` · `check_regression` · `guard_constraints` ·
`check_ci_integrity` · `validate_json`. `git status --porcelain` propre hors ce log +
le `daily_report`. **Zéro gate ajouté, zéro chiffre inventé, zéro fichier de code touché,
aucune commande VPS.** Le canal 2 est de nouveau à jour (2 addenda · 5 sessions couvertes).

## Sprint 8 · buffer · Audit d'EFFICACITÉ des gates anti-dérive : mutation-testing du suite complet → 7 mutations, 7 RED prouvés (« vert » ≠ « faux-vert ») + zéro gate ajouté (#5)

**Nature — assurance de phase launch, PAS un gate de plus (CLAUDE.md #5).**
La série récente a saturé la chasse aux *surfaces non couvertes* (mémoire
`verify-uncovered-before-gating`) : elle prouve qu'une dérive **serait attrapée SI**
un gate mordait. Angle complémentaire jamais fait explicitement en buffer : **les
gates verts mordent-ils réellement ?** Un gate cassé/neutralisé reste vert et donne
une fausse assurance — risque direct en phase de bascule launch (Sprint 8). J'ai donc
**mutation-testé le suite**, chaque mutation immédiatement révertée (`git checkout --`,
aucune trace ; arbre propre re-vérifié `git status --porcelain` = vide).

**Protocole — 7 mutations ciblées sur 4 gates, résultat attendu = ROUGE :**

| # | Gate | Mutation injectée | Verdict |
|---|---|---|---|
| M1 | `check_ci_integrity` | job fantôme `ghost-job` ajouté à `gate.needs` (DANGLING) | RED ✓ |
| M2 | `check_ci_integrity` | `mobile-app-config-tests` retiré de `gate.needs` (MISSING) | RED ✓ |
| M3 | `check_artifacts` | octet parasite ajouté à `crm/commissions/out/MANIFEST.json` (repro byte) | RED ✓ |
| M4b | `check_readme_claims` | README « 22/22 modules » → « 23/22 » | RED ✓ |
| M4c | `check_readme_claims` | README « 15 promesses » → « 16 » | RED ✓ |
| M5b | `guard_constraints` | URL `https://github.com/acme/thing` (usage réel — cité ici, `ci-allow`) | RED ✓ |
| M5c | `guard_constraints` | `stripe payments` (paiement interdit CLAUDE.md #10) | RED ✓ |

**7/7 → les 4 gates échantillonnés MORDENT.** Chaque ROUGE a été confirmé par le
message de violation exact (ex. M4b : *« Qualité 4Big · modules gated — README dit
23/22/100 MAIS artefact dit 22 »* ; M4c : *« Recette · promesses — README dit 16/8/7
MAIS artefact… »* ; M5b : *« Interdit détecté — GitHub »*). Les 3 autres gates
(`check_docs`, `check_regression`, `validate_json`) restent couverts par la campagne
« surfaces » antérieure ; échantillon centré ici sur les gates à **oracle recomputé**
(les plus exposés à un faux-vert par régression de logique).

**Non-finding documenté (piège évité) — `guard_constraints` ne flague PAS le mot nu
« GitHub », À DESSEIN.** Une 1ʳᵉ mutation (« GitHub is great. ») est restée VERTE ;
avant de crier au trou, lecture de `ci/guard_constraints.sh:60` : le terme interdit
est l'**usage** (`github\.com|git@github|github\.io`), jamais la simple **mention** —
même principe que le suppresseur `PROHIBITION` (l.25) qui ignore les lignes-rappels
de règle (« JAMAIS GitHub » de `CLAUDE.md` ne doit pas s'auto-flaguer). Le « trou »
était donc ma mutation mal conçue, pas une faille : re-testé avec un **vrai** usage
(M5b) → ROUGE. Leçon actée en mémoire (`guard-constraints-flags-usage-not-mention`)
pour qu'une session future ne re-panique pas sur ce faux-positif.

**Bilan.** Le suite anti-dérive n'est pas décoratif : **vert = réellement conforme**.
Zéro gate ajouté (aucune surface non couverte prouvée · #5), zéro fichier de code/doc
touché hors ce log, arbre propre. État inchangé et vert : 7 gates re-joués → exit 0,
câblage CI intègre (`gate.needs` = 30 jobs · `e2e-baseline` seul job manuel, vérifié).
Aucune commande touchant au VPS.


## Sprint 8 · buffer · Dérive doc-vs-réalité : 11 fiches AGENT.md pointaient un canal `handoffs/` INEXISTANT — correction vers le canal de hand-off RÉEL (livrables `out/` commités)

**Nature — dérive réelle, pas un gate de plus (CLAUDE.md #5 · interdit « documenter sans vérifier existence courante »).**
Audit de complétude plutôt qu'empilage : la série récente a saturé les surfaces
driftables numériques ; j'ai cherché une dérive doc-vs-réalité échappant aux gates.

**Constat.** Le bloc boilerplate « ## Communication inter-agents » (identique dans
**11** des 13 fiches `03_agents/*/AGENT.md`) affichait :
`- Handoffs formalisés dans \`05_deliverables_mvp/handoffs/\``. Or ce répertoire
**n'existe pas** — `find -iname '*handoff*'` = 0 fichier, 0 consommateur (code/gate),
0 test. Fausse promesse de canal. Les 2 autres lignes du bloc sont, elles, vraies
(`daily_reports/` existe · WhatsApp conforme CLAUDE.md).

**Pourquoi les gates l'ont manqué (angle mort documenté, pas à re-gater).**
`check_docs.sh` §1 valide les cibles de liens `[texte](cible)` mais **neutralise
délibérément les code-spans backtick** (l.28 `sed 's/\`[^\`]*\`//g'`) : un chemin
entre backticks est traité comme *exemple de syntaxe* (ex. `gen.py build|validate`
n'est pas un chemin réel). `\`…/handoffs/\`` étant un code-span, il échappe. **Gater
tous les backticks créerait des faux positifs massifs** (les fiches en regorgent :
entrées CLI, extensions) → j'ai **choisi de NE PAS élargir le gate** (anti-sur-gating,
mémoire `verify-uncovered-before-gating`). Le correctif est un correctif de vérité,
pas un nouveau gate.

**2 solutions (workflow #4) — la bonne = corriger la doc, pas créer le dossier.**
- ❌ Créer `handoffs/` + README index : inventerait du contenu (#6) et **dupliquerait**
  le mécanisme de hand-off déjà réel (les `out/` commités), violant #5.
- ✅ **Corriger les 11 lignes** vers le canal qui existe déjà, décrit richement dans
  chaque fiche (« Chaque générateur transforme un spec en fixtures `out/` commitées —
  le hand-off direct »). Nouvelle ligne, universellement vraie pour les 11 agents
  (chacun produit au moins un doc/spec/fixture commité, y compris bim/ifc_speckle/
  rendu/erpnext_backend qui n'ont pas de `out/` mais ont une fiche/SPEC) :
  `- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs \`out/\`, SPEC, README), consommés directement par l'agent destinataire`.

**Exécution.** Remplacement littéral exact (script Python, assertion `count==11`,
1 occurrence/fichier) sur les 11 fiches : bim · crm · devops · erpnext_backend ·
frontend_console · ifc_speckle · mobile · onapi_legal · qa · rendu · seo.

**Vérif.** (1) `grep -rn handoffs` sur `03_agents/ 05_deliverables_mvp/ ci/ README.md`
→ **0 résidu**. (2) Nouvelle ligne présente (échantillon crm l.68, bim l.74). (3)
**7 gates verts** : check_ci_integrity · check_artifacts · check_docs ·
check_readme_claims · check_regression · guard_constraints · validate_json (tous exit 0).

---

## Sprint 8 · buffer · Démo/scenarios — ancrage des MINIMA D'ORCHESTRATION (« ≥ 2 modules distincts · ≥ 3 beats ») sur la garde `check_invariants` (CODE-autorité)

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). Classe « seuil data-derived recopié dans une garde de validation +
sa prose, jamais ancré » (déjà couverte pour les planchers `min_doc_bytes` /
`min_test_methods` de l'auditeur 4Big). Un candidat restait NON gaté (grep :
absent de tous les `ci/*.sh`) au **Livrable Sprint 7** (`demo/scenarios`, run-sheet
de pitch P07/P05) : la **garantie #3** du README (« chaque scénario tisse **≥ 2
modules distincts** et **≥ 3 beats** ») énonce deux planchers structurels
RÉELLEMENT imposés par la garde `check_invariants` bloc (c) de
`demo_scenario_gen.py` — la SEULE autorité d'exécution : `len(sc["beats"]) < 3`
refuse un scénario sous 3 beats, `len(mods) < 2` sous 2 modules distincts. Les
deux nombres sont recopiés À LA MAIN à **3 surfaces**, aucune gatée : (1) la
comparaison `< N`/`< M` (le plancher EFFECTIF) · (2) le message d'erreur
« (min 3) »/« (min 2 · orchestration) » (bornes annoncées au caller) · (3) la
prose README.

**Le « vert trompeur » — PROUVÉ ungate avant ajout** (mémoire
`verify-uncovered-before-gating`) : muter « ≥ 3 beats »→« ≥ 4 »/« ≥ 2 modules »→
« ≥ 5 » dans le README laissait `check_readme_claims` **exit 0**. Resserrer la
règle (démo plus riche : `< 2`→`< 3` modules) sans toucher la prose ⇒ README
PÉRIMÉ en silence, documentant un plancher que le code n'applique plus — dérive
qu'aucune suite `tests/` (exerce la FONCTION `check_invariants`, jamais la prose)
n'attrape, invisible à `check_artifacts` (`.py` ≠ `out/*.json`).

**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Démo · run-sheet · MINIMA
D'ORCHESTRATION », juste après le bloc « modules cités ») — RE-DÉRIVE les 2 minima
depuis la SEULE comparaison `<` de la garde (source unique · zéro duplication #5)
et exige : (1) autorité = `<` de `check_invariants` ; (2) le message d'erreur
« (min N) »/« (min M · orchestration) » == plancher effectif ; (3) la prose #3 du
README == plancher effectif. Une surface INTROUVABLE échoue AUSSI (ancre morte ·
anti-évaporation #6).

**Vérif** — 3 checks verts sur l'arbre propre (≥ 2 modules · ≥ 3 beats). **7
morsures** adversariales, gate lancé SEUL, reverts par `cp` ciblé (jamais
`git checkout` large — cf. incident consigné) : **M1** README `≥3 beats`→`≥4` (3) ·
**M2** README `≥2 modules`→`≥5` (3) · **M3 SILENT-GREEN** garde `<3`→`<4` beats
(code resserré, prose+message figés) ⇒ **message ET README mordent** (preuve que
l'autorité est le CODE, pas la cohérence interne de la prose) · **M4** message
`(min 3)`→`(min 4)` désync de `<` ⇒ message mord, README reste OK · **M5** garde
`<2`→`<3` modules ⇒ message ET README mordent · **M6** prose #3 supprimée ⇒
INTROUVABLE · **M7** comparaison `<` de la garde retirée ⇒ autorité INTROUVABLE.
Arbre restauré (`git status` propre, seule l'addition au gate subsiste). Suite
unittest re-verte : **39 tests OK** (générateur inchangé). Suite CI complète
re-verte : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0.

## Sprint 8 · buffer · CRM/workflow_vente — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate_bundle` (9ᵉ instance du ledger · dialecte `# Invariant N ·`)

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). La classe « ledger d'invariants » (recompute le nombre de contrôles
CI depuis le registre-source des marqueurs numérotés de la fonction de validation,
jamais une liste à la main) était consignée « close à 8 générateurs » (Chat OTOIA ·
SEO · Fiscal e-CF · Legal CONFOTUR · CRM Dossier Vente · QA Audit 5D · CRM
Commissions · Mobile) + 2 (deploy_runbook/acceptance, dialecte `# INVn`). La mémoire
`invariant-ledger-count-gate` avertit précisément : « CLOSED a été prouvé FAUX DEUX
FOIS ; re-scanner TOUS les dialectes de marqueur AVANT de croire la clôture ». Un
**3ᵉ oubli** a été exhumé : `crm/workflow_vente/workflow_vente_gen.py` (Livrable
Sprint 4 l.50 · workflow de vente Frappe lead→…→CONFOTUR) porte un ledger de **9
invariants métier de graphe** dans `_validate_bundle`, marqueurs `# Invariant N ·`
(style Mobile), phrasé « schéma + 9 invariants ». Le nombre « 9 » est recopié À LA
MAIN à **4 surfaces vives**, aucune gatée : générateur l.223 (message CLI de succès)
· README l.78 (bloc de commande) · README l.114 (prose de vérification) · README
l.125 (prose d'auto-score).

**Le « vert trompeur » — PROUVÉ ungate avant ajout** (mémoire
`verify-uncovered-before-gating`) : muter « 9 invariants »→« 10 » dans le README
laissait `check_readme_claims` **exit 0**. Ni `check_artifacts` (le `.py` source
n'est pas un `out/*.json`) ni les suites `tests/` (FONCTIONS de validation, jamais
la prose) n'attrapent la dérive. Ajouter un contrôle `# Invariant 10 ·` à
`_validate_bundle` sans toucher les 4 chaînes ⇒ README + self-report CLI mentent en
silence pendant que la CI applique 10 contrôles.

**Particularités du registre** (vs les 8 blocs `# N ·`) — style `# Invariant N ·` +
fonction `_validate_bundle` (comme Mobile ; les blocs ancrés `# N ·`/`def _validate\b`
ne le voient pas). Ledger PROPRE 1..9 compté À PART du schéma (« schéma + N
invariants » · pas de schéma-#1, pas de split a/b ; filet doublon nu conservé). Un
SECOND nombre distinct — « **25 tests** » (suite unittest, déjà gatée) — présent au
README : l'ancre sur le mot « invariants » l'exclut nativement ⇒ **aucun faux
positif**. Le README N'A NI titre « ## Les N invariants » (son titre « ## Invariants
vérifiés par le CLI » ne porte aucun chiffre) NI énumération 1:1 avec le ledger — sa
liste compte le SCHÉMA en item 1 puis **FUSIONNE les invariants 8 & 9** dans son item
9 (« Maîtres … ; comptes du manifeste cohérents ») : elle N'EST PAS le ledger, la
gater comme contiguë==ledger serait un faux positif fragile (présentation éditoriale).
On gate donc UNIQUEMENT (a)(b)(d), comme Audit 5D.

**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « CRM · workflow_vente · IDENTITÉ
du COMPTE D'INVARIANTS », juste après le bloc Mobile) — RECOMPUTE le ledger depuis
le SEUL registre-source (les `# Invariant N ·` de `_validate_bundle`, corps extrait
jusqu'au `def` suivant) 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|. Mention ou
registre absent = échec (anti-évaporation #6).

**Vérif** — 3 checks verts sur l'arbre propre (ledger 1..9 = 9 marqueurs `# Invariant
N ·`, schéma compté à part). **7 morsures** adversariales, gate lancé SEUL, reverts
par `cp` ciblé (jamais `git checkout` large — cf. incident consigné) : **M1** `.py`
9→10 (b) · **M2** README 9→10 métier (d) · **M3 SILENT-GREEN** `# Invariant 10 ·`
injecté ⇒ ledger→10 pendant que la prose reste 9 (mord `.py` ET README, preuve que
l'autorité est le CODE) · **M4** `# Invariant 5 ·` supprimé ⇒ ledger non contigu (a) +
self-reports désync · **M5** doublon nu `# Invariant 3 ·` ⇒ vrai doublon (a) · **M6**
toutes les mentions « 9 invariants » retirées du README ⇒ self-report ÉVAPORÉ · **M7
no-false-positive** : muter « 25 tests »→« 26 » ⇒ le gate ledger reste SILENCIEUX
(l'ancre « invariants » l'exclut ; le rouge vient du gate test-count préexistant, pas
du mien). Arbre restauré (`git status` propre, seule l'addition +113 subsiste). Suite
unittest re-verte : **25 tests OK** (générateur inchangé). Suite CI complète re-verte :
`check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` ·
`check_regression` · `check_readme_claims` tous exit 0. **La classe ledger passe à 9
générateurs (+2 dialecte `# INVn`) ; re-scanner tout NOUVEAU dialecte AVANT de
re-déclarer close — ce 3ᵉ oubli confirme l'avertissement de la mémoire.**

## Sprint 8 · buffer · Faisabilité/bancable — ÉLIMINATION du taux `0.52` codé en dur dans l'oracle anti-invention (source unique CANONICAL · #6/#5)

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres » · workflow #5 « zéro duplication »). En balayant les copies MANUELLES
du paramètre canonique #9 « point d'équilibre 52 % » restées HORS de la source
unique, une a été trouvée au **cœur de l'oracle anti-invention** du générateur
bancable : `faisabilite/bancable/bancable_gen.py::_check_derived_arithmetic`
(l.131) recopiait le taux en **littéral `0.52`** (`exp_pe = math.ceil(0.52 *
exp_total)`) pour re-calculer indépendamment le point d'équilibre et prouver
qu'« aucune valeur n'a été posée ». Paradoxe : le module PRÊCHE l'inverse — son
`deps.py` (« une seule source de vérité pour tout le mandat ») importe
`CANONICAL = genlib.model.CANONICAL` (gaté sur CLAUDE.md #9/#10) précisément
pour NE PAS dupliquer `3 % / 8.5 % / 52 %`, et `finance._pct` porte le docstring
explicite « **jamais codé en dur ici** — évite toute dérive ». Le `0.52` de
l'oracle était le SEUL survivant en dur (grep : aucun autre `0.03/0.085/0.52`
littéral dans le code du module ; `finance.py`/`report.py` passent tous par
`CANONICAL`).

**Pourquoi un FIX et pas un gate** — mutation-test préalable (mémoire
`verify-uncovered-before-gating`) : AUCUNE mutation ne produit un « vert
trompeur ». Le `0.52` est **transitivement protégé** — l'invariant du module
compare l'oracle à la figure `finance` (issue de CANONICAL) au moment de la
génération, et le gate bancable (`check_readme_claims.sh`) LANCE cette génération
(`bancable_gen validate`) ; toute divergence `0.52`≠CANONICAL casse `validate`
→ gate ROUGE. Un gate d'ancrage du littéral serait donc **redondant** (interdit
#5). MAIS le `0.52` en dur est un **BUG LATENT** de sens inverse : un changement
LÉGITIME du mandat (#9 52 %→55 %, propagé à CANONICAL par les gates model/
template) laisserait l'oracle à 21 pendant que `finance` produit 22 ⇒ le
générateur **REFUSERAIT en silence un dossier bancable VALIDE** (invariant
« manifeste 22 != recalcul 21 »), avec un message cryptique « génération réelle
échoue » côté CI. La bonne réponse n'est pas de gater la copie, mais de la
**supprimer** — dériver le taux de la source unique, comme le fait déjà
`finance.py`.

**Correctif** (`bancable_gen.py`, `_check_derived_arithmetic`) — `0.52` remplacé
par `pe_pct = finance._pct(deps.CANONICAL["point_equilibre_pct"])` (RÉUTILISE le
parseur `%→fraction` déjà livré · zéro nouvelle logique) + garde `pe_pct is not
None`. Docstring l.117 réécrit : le taux est « lu à la SOURCE UNIQUE (CANONICAL ·
CLAUDE.md #9, gaté), jamais recopié en dur ici ». L'indépendance de l'oracle est
préservée là où elle compte (Σ quantités · × prix · `ceil`) ; le TAUX, lui, est
une constante de mandat déjà ancrée en amont (CANONICAL gaté sur CLAUDE.md #9),
donc le re-dériver de cette source n'affaiblit rien.

**Vérif** — `validate fixtures/brief_bancable.json` byte-identique à 52 %
(`point_equilibre_unites = 21 = ⌈52 % × 40⌉`). **22 tests** unittest verts
(inchangés · oracles fixture self-consistants). **Preuve du bug levé** : en
simulant la source à 55 % (`deps.CANONICAL["point_equilibre_pct"]="55 %"`), les
invariants restent **OK** et `point_equilibre_unites` passe à **22 = ⌈55 % × 40⌉**
(AVANT le fix : oracle figé à 21 ⇒ invariant cassé ⇒ dossier valide rejeté).
Suite CI complète re-verte : `check_artifacts` · `check_docs` ·
`guard_constraints` · `check_ci_integrity` · `check_regression` ·
`check_readme_claims` tous exit 0. **Leçon** : toute copie manuelle d'une
constante de mandat n'appelle pas un gate N+1 — quand elle est déjà
transitivement protégée, la SUPPRIMER (source unique) vaut mieux qu'un checker
redondant (#5), et lève au passage le bug-miroir « le mandat change → le module
casse ».

## Sprint 8 · buffer · Ledger d'invariants — 2ᵉ FAMILLE (`# INVn ·` / `validate_bundle` / « N familles d'invariants ») : ancrage du COMPTE de `deploy_runbook` (11) + `acceptance` (11)

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). La classe « ledger d'invariants » (recompute le nombre de contrôles CI
depuis le registre-source des marqueurs numérotés de la fonction `_validate`, jamais
une liste à la main) avait été **déclarée close à 8 générateurs** — mais sur le SEUL
dialecte `# N ·` / « N invariants » / `_validate`(`_validate_bundle`). La mémoire
`invariant-ledger-count-gate` consigne précisément le risque : « un prior *CLOSED
après 7* était FAUX — un style de marqueur différent avait glissé ; re-scanner LES
DEUX styles avant de croire la clôture ». Un **re-scan systématique** a exhumé une
**DEUXIÈME famille** entièrement HORS gate : marqueurs `# INVn ·` (préfixe `INV`)
dans `validate_bundle`, phrasé « N **familles d'**invariants ». DEUX générateurs la
portent :
- `devops/deploy_runbook/deploy_runbook_gen.py` — ledger `# INV1..INV11` dans
  `validate_bundle`, compte **« 11 familles d'invariants »** recopié à **3 surfaces
  vives** : générateur l.272 (message CLI de succès) · README l.73 (bloc de commande)
  · suite tests l.4 (docstring) ;
- `qa/acceptance/acceptance_gen.py` — ledger `# INV1..INV11` dans `validate_bundle`,
  compte à **2 surfaces vives** : README l.46 « 11 invariants » · suite tests l.4
  « 11 familles d'invariants ». (Le `.py` acceptance ne porte AUCUN compte numéroté
  → surface non exigée.)

**Le « vert trompeur »** — ajouter un contrôle `# INV12 ·` à `validate_bundle` sans
toucher ces chaînes ⇒ README + self-reports + docstrings de test mentent en silence
pendant que la CI applique 12 contrôles. Ni `check_artifacts` (le `.py` source n'est
pas un `out/*.json`) ni les suites `tests/` (FONCTIONS de validation, jamais la prose)
n'attrapent la dérive. **Prouvé (M4 SILENT-GREEN)** : injecter `# INV12 ·` dans
`validate_bundle` (ledger→12, toute la prose reste 11) ⇒ les **3 surfaces
deploy_runbook mordent** « [11] MAIS ledger en compte 12 » — preuve que l'autorité
est le **CODE-registre**, pas la cohérence interne de la prose.

**Particularités** — Ledger PROPRE 1..N compté À PART du schéma (« schéma + N
invariants »). Les mentions NON numérotées de « invariants » (docstrings « invariants
de déploiement/recette », README « Un invariant refuse … ») ne sont pas ancrées par
un chiffre → **aucun faux positif**. Le compte-regex accepte les DEUX formes
(« N invariants » ET « N familles d'invariants ») via un préfixe optionnel
`(?:familles d')?`. Les `daily_reports`/`activity_log` (snapshots datés — ex.
acceptance « 10 » d'avant la croissance 10→11) ne sont PAS des surfaces vives et ne
sont volontairement pas ancrés (aucune dérive à surveiller sur un log figé).

**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « DevOps deploy_runbook + QA
acceptance · IDENTITÉ du COMPTE d'INVARIANTS de `validate_bundle` », juste après le
bloc Mobile) — **paramétré sur les 2 modules** (zéro duplication). RECOMPUTE le
ledger depuis le SEUL registre-source (`# INVn ·` de `validate_bundle`, corps extrait
jusqu'au `def` suivant) et exige : **(a)** ledger CONTIGU 1..N (lacune OU doublon nu
= vrai défaut de registre) ; **(b)** CHAQUE surface déclarée porte ≥1 mention
« N [familles d']invariants » toutes == |ledger|. Une surface sans mention = self-report
ÉVAPORÉ (échec · anti-évaporation #6).

**Vérif** — 7 checks verts sur l'arbre propre (2 ledgers 1..11 + 5 surfaces).
**8 morsures** adversariales, gate lancé SEUL, reverts par `cp` ciblé (jamais
`git checkout` large — cf. incident consigné) : **M1** deploy `.py` 11→12 (b générateur)
· **M2** deploy README 11→12 (b) · **M3** deploy test 11→12 (b) · **M4 SILENT-GREEN**
`# INV12 ·` injecté ⇒ 3 surfaces deploy mordent (autorité = CODE) · **M5** `# INV7 ·`
supprimé ⇒ ledger non contigu (a) + 3 surfaces désync · **M6** acceptance README 11→12
(b) · **M7** acceptance test 11→12 (b) · **M8** compte retiré du README acceptance ⇒
self-report ÉVAPORÉ. Arbre restauré (`git status` propre). Suites unittest re-vertes :
deploy_runbook **29 tests OK** · acceptance **37 tests OK** (générateurs inchangés).
Suite complète re-verte : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0.
**Les DEUX familles de ledger sont désormais gatées ; re-scanner tout NOUVEAU
dialecte de marqueur (préfixe/phrasé) avant de re-déclarer la classe close.**

## Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'encadré « Défense en profondeur » (NOM du rôle détenteur de `set_user_permissions` · scope · famille) sur l'artefact byte-gaté

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention »). Le §2
« Modèle RBAC » de `05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` ferme sur un
encadré de sécurité « **Défense en profondeur** » : le pouvoir `set_user_permissions`
(élévation de privilège) est « **réservé** au SEUL rôle `OTO Plateforme RBAC Admin`
(`scope_donnees = groupe`, `famille = plateforme`) — séparation des pouvoirs vérifiée
par test automatisé ». La SPEC se réclame donc d'un **fait de sécurité tiré du
contrat** `rbac_50_roles.json`, sur **trois attributs** : NOM du détenteur · scope ·
famille. Or le test cité (`test_rbac::test_set_user_permissions_reserve_admin`) impose
bien scope==groupe ET famille==plateforme à TOUT détenteur — MAIS **jamais le NOM du
rôle** (`OTO Plateforme RBAC Admin`) NI cette prose §2. L'**UNICITÉ** (« au SEUL rôle »)
et le NOM étaient DÉJÀ gatés côté **artefact** (bloc « RBAC fixtures », singleton
`len(fx_sup)==1` depuis `custom_docperm.json` byte-gaté + prose du **README fixtures**
« émis que pour le rôle **NAME** ») — mais **PAS la 3ᵉ surface prose, le §2 de la SPEC**.

**Le « vert trompeur »** — RENOMMER le détenteur dans le contrat (+régénérer
fixtures/out + audit 4Big) laisse le §2 citer un rôle **DISPARU** en silence :
`check_artifacts` ne voit qu'un **HASH de TAILLE** du doc (agnostique au nom · la SPEC
n'est pas un `out/*.json`), les 10 tests restent verts (scope/famille inchangés).
**Prouvé (M6 SILENT-GREEN)** : renommer `…RBAC Admin`→`…Securite Admin` dans le
contrat + régénérer les 4 générateurs RBAC & l'audit ⇒ **`check_artifacts` VERT**
pendant que le nouveau gate mord « §2 nomme ['OTO Plateforme RBAC Admin'] MAIS
set_user_permissions réservé à 'OTO Plateforme Securite Admin' » — preuve que
l'autorité est l'**ARTEFACT byte-gaté**, pas la cohérence interne de la SPEC.

**Gate ajouté** (`ci/check_readme_claims.sh`, **extension** du bloc « RBAC fixtures »,
points 4 & 5 — pas un bloc neuf) — RÉUTILISE `fx_sup` (le singleton déjà byte-gaté
ci-dessus · **zéro re-dérivation de l'unicité**, interdit #5 « jamais de doublon »)
comme **NOM-autorité** et `src_roles` (contrat byte-gaté) pour scope/famille. Capte
l'encadré `**Défense en profondeur** … vérifiée par test automatisé` (spec_rd
whitespace-normalisé) et exige : le pouvoir `set_user_permissions` y est **nommé** ;
**(a)** le NOM du détenteur cité (accents/casse neutralisés) == le seul porteur du
flag ; **(b)/(c)** les `scope_donnees = …` / `famille = …` cités == attributs du
détenteur dans `rbac_50_roles.json` ; **(5)** §6 (row « Séparation des pouvoirs »)
cite bien `set_user_permissions` réservé (anti-évaporation). Encadré/row INTROUVABLE =
échec (#6). La glose « (gestion des habilitations) » N'est PAS gatée (éditoriale).

**Note process** (interdit #5) — un premier jet ajoutait un bloc §2 autonome
RECALCULANT le singleton depuis `src_roles` : **doublon** de la vérification d'unicité
déjà portée par le bloc fixtures (via `custom_docperm.json` byte-gaté, équivalent par
`check_artifacts`). Écarté ; les 4 checks réécrits en **extension** du bloc existant,
sans ré-affirmer l'invariant d'unicité.

**Vérif** — 4 checks verts sur l'arbre propre. **6 morsures** adversariales, gate
lancé seul, reverts par `cp`/`tar` ciblés (jamais `git checkout` large — cf. incident
consigné) : **M1** §2 renomme le détenteur (prose) ⇒ NOM ≠ artefact · **M2** §2
scope `groupe`→`entite` ⇒ (b) ≠ détenteur · **M3** §2 famille `plateforme`→`ventes`
⇒ (c) ≠ détenteur · **M4** encadré supprimé ⇒ INTROUVABLE · **M5** §6 retire
`set_user_permissions` ⇒ ancre évaporée · **M6 SILENT-GREEN** (ci-dessus) contrat
renommé + artefacts régénérés, `check_artifacts` VERT ⇒ §2 (a) mord. 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 · 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.

---

## Sprint 8 · buffer · Mobile app_config : gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate_bundle` (8ᵉ instance)

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). La classe « ledger d'invariants » (recompute le nombre d'invariants CI
depuis le registre-source numéroté de la fonction de validation, jamais une liste à
la main) était déjà appliquée à **7 générateurs** : Chat OTOIA · SEO · Fiscal e-CF ·
Legal CONFOTUR · CRM Dossier Vente · QA Audit 5D · CRM Commissions. Le **8ᵉ**
générateur portant le même nombre magique restait NON couvert :
`mobile/app_config/app_config_gen.py`.

Le compte **« 12 invariants »** y est transcrit **à la main à QUATRE endroits**,
aucun gaté, tous invisibles à `check_artifacts` (le `.py`/`.md`/test n'est pas un
`out/*.json`) :
1. `app_config_gen.py` message de succès CLI « … schéma + 12 invariants verts. »
   (l.228) ;
2. `README.md` bloc de commande « schéma + 12 invariants » (l.48) ;
3. `README.md` prose de récap « Schéma + 12\ninvariants verts » (l.67-68 — le « 12 »
   et le mot séparés par un saut de ligne) ;
4. `tests/test_mobile_app_config.py` docstring « … + les 12 invariants du CLI »
   (l.4).

Ajouter un contrôle `# Invariant 13 ·` à `_validate_bundle` sans toucher les 4
chaînes ⇒ le README, le self-report CLI ET la docstring de test mentent en silence
pendant que la CI applique 13 contrôles. Aucune suite `tests/` (qui EXÉCUTE la
validation mais ne compte jamais la prose) ne l'attrape.

**Particularités vs les 7 blocs précédents** (documentées dans l'en-tête du gate) :
- le registre-source utilise le style **`# Invariant N ·`** (le mot « Invariant »
  précède le numéro) — les blocs ancrés sur le style nu `# N ·` NE le voient PAS ;
  ancrage sur `#\s*Invariant\s+(\d+)…` ;
- la fonction s'appelle **`_validate_bundle`** (pas `_validate`) — l'ancre `def
  _validate\b` des autres blocs échoue (`\b` échoue avant `_bundle`) ;
- ce README n'a NI titre « ## Les N invariants » NI énumération numérotée → on gate
  **(a)(b)(b')(d)**, en AJOUTANT la surface **fichier-de-tests (b')**, absente des 7
  blocs précédents (leurs tests ne portaient pas le chiffre).

**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Mobile · app_config · IDENTITÉ
du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source
(`# Invariant N ·` de `_validate_bundle`) et exige :
- **(a)** ledger CONTIGU 1..N (schéma compté à part ; lacune OU doublon nu = défaut
  mordu) ;
- **(b)** toute mention « N invariants » du `.py` == |ledger| ;
- **(b')** toute mention « N invariants » du fichier de tests == |ledger| ;
- **(d)** toute mention « N invariants » du README == |ledger|.

**Vérif** — 4 checks verts sur l'arbre propre (ledger 1..12 = 12 marqueurs). **8
morsures** adversariales : M1 self-report CLI 12→13 (b) · M2 README bloc-commande
12→13 (d) · M3 README récap (newline-split) 12→13 (d, preuve que `\s+` traverse le
saut de ligne) · M4 docstring test 12→13 (b', la surface neuve) · M5 ajout marqueur
`# Invariant 13 ·` ⇒ ledger→13 pendant que les 3 surfaces prose restent 12 (b+b'+d,
preuve que l'autorité est le CODE, pas la prose) · M6 suppression `# Invariant 7 ·`
⇒ gap non-contigu (a) + désync prose · M7 doublon nu `# Invariant 5 ·` ⇒ détection
de vrai doublon · M8 renommage `_validate_bundle` ⇒ registre introuvable. Gates
re-verts : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_readme_claims` · `check_regression` tous exit 0.

---

## Sprint 8 · buffer · Doc QA : documentation du 2ᵉ étage QA (batterie de gates statiques `ci/*.sh`) dans `03_agents/qa/AGENT.md`

**Démarche — vérifier AVANT de gater (mémoire `verify-uncovered-before-gating`).**
La série récente empile des gates anti-dérive ; avant d'en ajouter un 9ᵉ, j'ai
d'abord cherché une **vraie dérive présente** plutôt qu'un gate spéculatif :

1. Scan ledger-vs-prose sur les 12 générateurs à ledger d'invariants. **Faux
   signal** relevé : `fiscal/ecf_dgii` semblait afficher `ledger_markers=10` vs
   `prose=12`. Investigation : le ledger n'est **pas** nu 1..12 — l'invariant #1
   (validation de schéma, `validate(bundle, schema)`) n'est **pas** marqué `# 1 ·`
   par design, et #12 est scindé en `12a`/`12b`. Le gate fiscal (`check_readme_claims.sh`
   l.1908) **synthétise** déjà `ledger = marqueurs{2..12} ∪ {1}` et vérifie
   self-report / titre README / mentions / énumération = 12. **Aucune dérive** ;
   mon compteur grossier ne voyait ni le schéma-#1 ni `12a/12b`.
2. Sondé les comptes de `03_agents/{crm,qa}/AGENT.md` (« Total CRM 81 (25+31+25) »,
   « 17 contrôles / 5 dimensions », « 8 livrables + 7 métriques ») → **tous gatés**
   (l.681, 871, 2176, 2624, 3671…). Coverage **très dense**, conforme à la mémoire.

**Conclusion** : pas de gate à ajouter (ç'aurait été redondant · CLAUDE.md #5).

**Trou documentaire réel comblé.** Le titre + le scope de `qa/AGENT.md` promettent
des « **gates de méta-niveau** », mais le corps ne tabulait que les **4 modules
Python** d'audit (`audit_5d` · `audit_4big` · `regression` · `acceptance`) — il
**omettait entièrement** la batterie de **gates statiques `ci/*.sh`**, qui sont
pourtant l'application méta-niveau littérale (moitié de l'enforcement QA). Un
lecteur croyait donc QA = 4 modules.

**Ajout** — section « Deuxième étage QA : la batterie de gates statiques (`ci/*.sh`) »
listant les 7 gates courants par **nom + rôle** (`check_ci_integrity`,
`check_readme_claims`, `check_artifacts`, `check_regression`, `check_docs`,
`guard_constraints`, `validate_json`). **Anti-dérive by design** : le texte ancre
l'autorité du périmètre sur `check_ci_integrity` (qui **découvre** l'ensemble via
`git ls-files 'ci/*.sh'` et refuse tout gate décâblé/orphelin) et présente la liste
comme *l'état courant*, PAS une constante figée → aucun compte agrégé driftable
introduit.

**Sûreté** — prose sans lien Markdown neuf (check_docs), sans URL git interdite ni
terme CRM/paiement banni nu ; les lignes citant les prohibitions portent un
marqueur (`proscrites`/`interdits`/`bannie`) donc `guard_constraints` les laisse
passer. **Vérif** : 7 gates re-verts (guard_constraints · check_docs ·
check_readme_claims · check_ci_integrity · check_artifacts · check_regression ·
validate_json tous exit 0).

---

## Sprint 8 · buffer · Angle mort du gate « comptes de tests par suite » : la cellule `rbac` (fiche pointant une SPEC, pas un README)

**Démarche — vérifier AVANT de conclure « couvert » (mémoire `verify-uncovered-before-gating`).**
La série récente ayant épuisé les surfaces driftables évidentes, j'ai audité la
**complétude d'un gate existant** plutôt que d'en empiler un neuf (CLAUDE.md #5).

**Constat.** Le gate `check_readme_claims.sh` §« Fiches agents · comptes de tests
PAR SUITE (colonne « Tests ») » recompute chaque cellule `| N |` des tables de
livrables des `03_agents/*/AGENT.md` depuis l'autorité `regression_plan.json`
(`plan.suites[path].test_methods`). Son `row_re` exigeait pourtant que le lien de
la ligne pointe `…/README.md`. **Or une ligne y échappe** : la fiche
`erpnext_backend` (l.28) pointe le module `rbac/` via son **point d'entrée
`RBAC_50_ROLES_SPEC.md`** (le contrat 50-rôles, pas un README) — donc son
`row_re` ne mordait pas, et sa cellule « Tests | 10 | » restait **non vérifiée**.

**Mutation-test (preuve du trou).** Cellule `rbac` `10 → 99` sur la fiche →
`check_readme_claims` restait **VERT** (exit 0), aucune ligne `rbac` évaluée. C'est
exactement la classe « vert trompeur » que cette section du gate existe pour tuer
(un régénérateur de fiche pourrait y écrire un faux compte sans que la CI bronche).
La somme prose « RBAC 60 tests (10+11+12+11+16) » est **order-indépendante** → ne
contraint pas la cellule-table `rbac` individuelle. Vrai angle mort.

**Correctif (extension du gate, pas gate N+1).** `row_re` accepte désormais tout
`*.md` d'entrée du dossier module (`/[\w.-]+\.md` au lieu de `/README\.md`), pas
seulement README — le garde `path not in auth` (déjà présent) protège de tout
élargissement fortuit : seules les lignes pointant une **suite gatée connue** sont
évaluées, les autres restent ignorées. Une seule ligne entre ainsi dans le
périmètre : `rbac`. Commentaire mis à jour pour documenter le cas SPEC.

**Vérif.** (1) Arbre propre VERT + la ligne `rbac` est désormais **VUE**
(`AGENT.md:28 · rbac — Tests 10 == source (10)`). (2) Mutation `10→99` désormais
**MORDUE** (`Tests 99 MAIS source dit 10 → régénérer la fiche`, exit 1). (3)
Restauration → exit 0. **7 gates re-verts** : check_ci_integrity · check_artifacts ·
check_docs · check_readme_claims · check_regression · guard_constraints ·
validate_json tous exit 0.

---

## Sprint 8 · buffer · Reprise du canal `daily_reports/` (lapsus depuis 2026-07-30) + sweep launch-readiness sourcée

**Constat.** Le canal **canonique** des rapports quotidiens
(`05_deliverables_mvp/daily_reports/YYYY-MM-DD.md` · fixé roadmap §Escalations,
référencé par les **13 fiches** `03_agents/*/AGENT.md`) n'avait **plus d'instantané
consolidé depuis le `2026-07-30`**. Or ~2 douzaines de sessions buffer ont continué
depuis — leur trace fine vit dans `05_activity_log/` (journal **technique par
session**), mais **aucun snapshot MVP sourcé** n'existait pour la phase Sprint 8.
Trou de reporting réel au niveau **stakeholder/direction**.

**Décision (workflow #5 — pas de gate redondant).** La série récente ayant épuisé
les surfaces driftables évidentes en durcissant des gates, et la phase Sprint 8
attendant une **vérification de régression exhaustive** (pas un module neuf), j'ai
(a) mené une **sweep de vérification launch-readiness** et (b) **repris le canal
`daily_reports/`** plutôt que d'empiler un gate N+1.

**Réalisé.**
- Créé `05_deliverables_mvp/daily_reports/2026-08-02.md` : instantané MVP **100 %
  sourcé** (chaque chiffre re-vérifié contre l'artefact commité · anti-invention #6),
  + **note de convention** réconciliant les 2 canaux de journalisation
  (`05_activity_log/` = technique/session · `daily_reports/` = synthèse/direction —
  pas des doublons ; un lecteur d'`AGENT.md` était mal aiguillé vers un canal figé au
  30/07).
- **Audit doc-vs-réalité de 2 fiches** (interdit « documenter du code sans vérifier
  son existence courante ») : `crm/AGENT.md` table livrables **exacte** (25/31/25 = 81,
  recomptés depuis `regression_plan.json`) ; `bim/AGENT.md` — son seul point de contact
  commité `faisabilite/generator/genlib/renderer.py` **existe** et rend bien le Volet 4
  Ingénierie en `{{…}}`. Aucune sur-promesse.

**État courant sourcé** (recalculé indépendamment, concordant) : **22/22** modules
qualité 4Big à 100/100 (`PASS`) · régression **22 suites · 564/564** tests
(`PASS`) · recette **15/15** promesses `in_repo` (`true`) · **13 agents** · **7 gates
CI** verts + reproductibilité `out/` byte-identique.

**Vérif.** Les **7 gates re-verts** (exit 0) avec le nouveau fichier :
check_docs (liens du rapport résolus) · guard_constraints (aucun terme interdit
introduit) · check_readme_claims · check_ci_integrity · validate_json ·
check_artifacts · check_regression. **Sûreté** : rapport dans le canal canonique
(roadmap), zéro chiffre inventé, tous liens Markdown vers cibles existantes.

---

## Sprint 8 · buffer · Audit d'exactitude doc-vs-réalité des 11 fiches `AGENT.md` restantes — 0 dérive réelle · 2 « faux positifs » d'audit levés · 1 clarification de lisibilité (faisabilite)

**Nature** — Poursuite de la tâche explicitement recommandée par le daily report
`2026-08-02` (« Poursuivre l'audit d'exactitude des 11 fiches AGENT.md restantes,
mêmes contrôles que crm/bim — repérer toute sur-promesse d'artefact ou compte
périmé »). CLAUDE.md interdit « documenter du code sans vérifier son existence
courante ». **Pas un gate de plus** (workflow #5) : vérification, pas empilage.

**Méthode** — 4 agents `Explore` en parallèle sur les 11 fiches (devops ·
erpnext_backend · faisabilite · frontend_console · ifc_speckle · mobile ·
onapi_legal · publiciste · qa · rendu · seo). Pour chaque fiche : existence de
CHAQUE chemin in-repo cité · recompute de CHAQUE compte (tests via `grep -c
'def test_'` / `regression_plan.json` · invariants via marqueurs numérotés) ·
présence de CHAQUE artefact `out/*.json` · toute sur-promesse d'un livrable
in-repo inexistant.

**Résultat brut des agents** — 9 fiches « FULLY ACCURATE » (comptes de tests
recoupés : deploy_runbook 29 · e-CF 39 · confotur 44 · publiciste 23 · audit_5d 37
· audit_4big 34 · regression 26 · acceptance 37 · seo 36 · portails 19 · chat 31 ;
SEO 258 mots-clés / 10 nœuds schema.org / 10 pages hreflang ; Expo SDK 54). **2
fiches signalées en « dérive »** — toutes deux **VÉRIFIÉES FAUX POSITIFS** après
contrôle manuel (mémoire `verify-uncovered-before-gating` : prouver avant d'agir) :

1. **onapi_legal** (agent : 3 chemins « CRITICAL cassés » l.40/49/99 :
   `../rbac/rbac_50_roles.json`, `../crm/workflow_vente/[out/]`). **Faux positif** :
   ces chemins backtick sont **relatifs à l'arbre des livrables** (`05_deliverables_mvp/legal/`,
   sujet de la fiche §24), PAS à l'emplacement de la fiche. Ils **MIROITENT le code**
   du générateur : `rbac_scan.py` fait `os.path.join(_DELIVERABLES, "rbac",
   "rbac_50_roles.json")` et `builder.py` publie `workflow_source:
   "crm/workflow_vente/workflow_vente_spec.json"`. Les 3 cibles existent bien depuis
   `05_deliverables_mvp/legal/` (vérifié `ls`). L'agent avait supposé à tort une
   résolution fiche-relative. Convention documentée dans la mémoire
   `backtick-path-escapes-check-docs` (les code-spans backtick sont volontairement
   NON link-checkés). **Aucune correction — la fiche est exacte.**

2. **faisabilite** (agent : `data_room/_TEMPLATE_…/`, `_META/version.json`,
   `otoia/capabilities/knowledge/faisabilite_template_v{X}.md` « HIGH · sources
   inexistantes »). **Faux positif** : ces chemins sont **explicitement VPS-runtime /
   « Composants à créer »** — préfixe absolu `/opt/oto/…`, placeholder de version
   `{X}`, section §95 « Composants à créer ». La fiche les réconcilie déjà (§15-17
   « composants runtime OTOIA/VPS … restent hors périmètre worker » ; §27-29 « aucune
   faisabilité concrète PXX n'est commitée ici … le rendu réel … s'exécute côté
   OTOIA/VPS #8 »). Hors-repo ASSUMÉ, pas une sur-promesse. **Table §Livrables
   exacte** : `TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md` (existe) · generator 16 tests ·
   bancable 22 tests. **Aucune sur-promesse in-repo.**

**Seule action — clarification de LISIBILITÉ (faisabilite), pas un correctif de
dérive.** Le point qui a induit l'agent en erreur est une vraie ambiguïté pour un
lecteur humain : le bloc « Sources canoniques » (§31-34) listait 3 chemins VPS
`{X}` sans signaler que la **contrepartie commitée/testée/gatée** du template
canonique est l'in-repo `TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md` (pourtant déjà en
tête de la table §Livrables). Ajout d'une **note additive** (style de réconciliation
déjà employé §15-17) : bandeau « Chemins VPS runtime — hors périmètre worker (#8),
non commités in-repo » + lien Markdown vers le template committé « référence de
vérité pour le worker ». **Zéro chiffre, zéro constante, zéro fait inventé** ;
purement désambiguïsant.

**Sûreté / vérif** — Avant édition : `grep` confirme qu'AUCUN gate ne parse la fiche
faisabilite ni la section « Sources canoniques » (édition prose libre). Après :
**7 gates re-verts (exit 0)** — `check_ci_integrity` · `check_artifacts` ·
`check_docs` (le nouveau lien Markdown résout vers une cible existante) ·
`check_readme_claims` · `check_regression` · `guard_constraints` (aucun terme
interdit introduit) · `validate_json`. **Bilan audit** : les **13 fiches**
`AGENT.md` sont désormais TOUTES auditées exactes (crm/bim la session précédente +
les 11 ici) ; les 2 « dérives » signalées étaient des artefacts de convention
(backtick deliverables-relative · chemins VPS/futurs assumés), pas des dérives.

---

## Session 20260802_120544 · Clôture de journée du canal `daily_reports/`

**Contexte** — Reprise en phase **Sprint 8 · buffer** (tous sprints 1→7 livrés
côté repo). Relu CLAUDE.md, la roadmap, MEMORY.md et le journal du jour. Constat :
la file de travail substantielle est **saturée** — 7 gates verts, 22/22 qualité,
564/564 régression, 15/15 recette, **13/13 fiches AGENT.md auditées exactes**
(campagne close au commit `2bc3afb`, session `113542`).

**Décision** (workflow #5 · pas d'empilage de gate redondant ; #6 · anti-invention)
— Aucune tâche roadmap ouverte ni blind-spot prouvé. Plutôt qu'un gate N+1 ou un
faux « module », **réconcilier le reporting stakeholder** : le snapshot canonique du
jour `05_deliverables_mvp/daily_reports/2026-08-02.md` (écrit à 11:11) était devenu
**vérifiablement périmé** — sa propre « Prochaine tâche #2 » (auditer les 11 fiches
restantes) et sa réserve `−4` (« audit limité à 2/13 fiches ») ont été **closes à
11:35** par le commit `2bc3afb`.

**Réalisé**
- **Recompute indépendant** (`python3` sur les artefacts commités) de TOUS les
  agrégats de tête, concordant : qualité **22 modules PASS** (`pass_score` 95) ·
  régression **22 suites / 564 ran / 564 passés / 0 échec** · recette **matrice de
  15 items, verdict `true`** · **13** répertoires `03_agents/*/`.
- **Addendum de clôture ADDITIF** au snapshot `2026-08-02.md` (corps 110539 laissé
  intact comme record historique) : acte la clôture 13/13 fiches, **lève la réserve
  −4** caduque, réaffiche l'état re-vérifié (inchangé/vert), révise la « prochaine
  tâche » (file d'audit vide → veille buffer). Zéro chiffre inventé — chaque valeur
  re-dérivée d'un artefact.
- **Choix anti-doublon** (interdit #5 · « JAMAIS accumuler doublons ») : addendum au
  fichier-jour canonique plutôt qu'un énième fichier per-session quasi-identique.

**Vérifications** — **7 gates CI re-joués → exit 0** (`check_ci_integrity` ·
`check_artifacts` byte-identique · `check_docs` liens résolus · `check_readme_claims`
· `check_regression` · `guard_constraints` aucun terme interdit · `validate_json`).
Aucune commande touchant au VPS.

**Non fait (hors périmètre #8)** — déploiement serveur réel, runner Gitea, Playwright
serveur : VPS `153.75.250.214`, plan ordonné `devops/deploy_runbook`.

---

## Session 20260802_123546 · Dérive « 12 agents » sur les docs de mandat de tête (hors README)

**Contexte** — Reprise en **Sprint 8 · buffer**. Relu CLAUDE.md, roadmap (0 item
ouvert), MEMORY.md, journal du jour. Re-vérification indépendante multi-angle de la
santé (recompute `python3` sur artefacts commités) : **7 gates → exit 0** · qualité
**22/22 @ 100/100** (verdict PASS · scores tous à 100, pas seulement le seuil) ·
régression **22 suites / 564 tests / 0 échec**, **13/13 modules** présents dans la
matrice · recette **15/15 `in_repo`** · constantes canoniques (3 %/8.5 %/52 %) toutes
ancrées · README (13 agents ×2, 22/22, 15 promesses) exact vs artefacts.

**Blind-spot RÉEL trouvé** (surface distincte de l'audit fiches `03_agents/*` clos au
`2bc3afb`) — audit des **docs de mandat de TÊTE**, non couverts par le gate de claims
(scopé README.md uniquement). `PORTAIL_BANCABLES_4BIG.md` vérifié exact (commande +
sorties `50_financier_bancable/{fr,en,es}.md` concordent avec `bancable_gen.py`). Mais
**deux docs affichaient encore « 12 agents »** :
- `AGENTS_EXISTING_ASSETS.md:1` (titre) · `02_master_prompt/MASTER_PROMPT_DTP_v2.md:28`.

Réalité re-dérivée : **13 agents** — 13 répertoires `03_agents/`, README canonique
« 13 agents » (×2, déjà gaté), le 13e (**publiciste**) **LIVRÉ+GATÉ** (fiche +
générateur `05_deliverables_mvp/publiciste/` **100/100** + suite de régression
`publiciste`). Les « 12 » précédaient l'ajout du publiciste → dérive doc-vs-réalité.

**Réalisé**
- **Correction du compte** 12 → 13 dans les deux docs (fait objectif, `03_agents/`
  = source unique · #6 anti-invention).
- **Clarification publiciste** (`AGENTS_EXISTING_ASSETS.md` §13) : marqueur « (à créer) »
  levé, **note « Statut mandat : LIVRÉ »** pointant fiche + générateur gaté, tout en
  **préservant** la liste « À développer » comme **runtime VPS** hors périmètre worker
  (#8) — respecte la convention « VPS-runtime `à créer` ≠ dérive » (memory
  `fiche-accuracy-audit-closed`). Deux liens Markdown ajoutés → résolus (`check_docs`).
- **Extension du gate** `check_readme_claims.sh` (surface prouvée NON couverte par
  mutation-test) : nouvelle boucle qui **réutilise le `n_agents` déjà dérivé** de
  `git ls-files 03_agents/*/AGENT.md` (pas de nouvelle machinerie · #5 pas de doublon)
  et exige que les DEUX docs de tête citent exactement ce compte. Classe
  `doc-numeric-claims-gate`.

**Vérifications** — nouveau check **mutation-testé** (12 réinjecté → gate ROUGE
« dit 12 MAIS 03_agents/ en compte 13 », restauré → exit 0). **7 gates re-joués →
exit 0**. Aucune commande touchant au VPS.

**Non fait (hors périmètre #8)** — runtime VPS publiciste (`publiciste.py`, systemd/
inotify, WhatsApp), déploiement serveur : hors périmètre worker.

---

## Sprint 8 · buffer · Compte d'agents — ancrage des 3 surfaces vives de `GAP_ANALYSIS_SPRINT1.md` (dont la coche de couverture DoD) sur la source unique `n_agents`

**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de chiffres »).
Le commit précédent (cb8d931) a ancré « 13 agents » à `git ls-files 03_agents/*/AGENT.md`
(source unique `n_agents`) sur les 2 docs de mandat de tête (AGENTS_EXISTING_ASSETS titre +
MASTER_PROMPT §28), en plus du README ×2. Un balayage de complétude a exhumé une **3ᵉ
surface entièrement HORS gate** : le **livrable Sprint 1** `05_deliverables_mvp/
GAP_ANALYSIS_SPRINT1.md` (Definition of Done) retranscrit ce même compte à **trois
endroits vifs** — l.12 périmètre d'audit (« audit des **13 agents DTP** »), l.53 constat
global (« **12 des 13 agents** partent d'un socle existant »), et surtout l.214 la **coche
de couverture DoD** (« Couverture **13 agents** + OTOIA core ✅ »). Cette coche AFFIRME une
complétude COURANTE : elle DOIT suivre le nombre réel de fiches.

**Le « vert trompeur » — PROUVÉ ungate avant ajout** (mémoire
`verify-uncovered-before-gating`) : `GAP_ANALYSIS` n'est référencé par AUCUN `ci/*.sh`
(`grep` = 0) ; muter l.214 « 13 »→« 14 » laissait `check_readme_claims` **exit 0**. Or ce
NOMBRE a une **dérive prouvée** : le publiciste fut ajouté comme 13ᵉ agent (12→13), forçant
la correction de cb8d931. Si un 14ᵉ agent était mandaté, la coche DoD mentirait en silence.
`check_artifacts` est aveugle (la SPEC n'est pas un `out/*.json`) ; aucune suite `tests/` ne
touche l'ancre doc↔disque.

**Gate ajouté — EXTENSION de la boucle head-docs existante** (`ci/check_readme_claims.sh`,
bloc « Comptes d'agents dans les docs de mandat de tête »), pas un bloc neuf : 3 tuples
`(GAP, motif, label)` réutilisant le `n_agents` DÉJÀ dérivé (zéro nouvelle machinerie ·
zéro re-dérivation · #5). On n'ancre QUE le **total** — le « 12 » analytique de l.53
(agents à socle existant) N'EST PAS gaté (motif `\d+ des (\d+) agents` capture le 2ᵉ nombre).
Surface INTROUVABLE = échec (anti-évaporation #6). `ci/README.md` mis à jour : le décompte
d'agents est désormais documenté comme ancré à **7 surfaces vives**.

**Vérif** — 3 checks verts sur l'arbre propre (les 3 == 13). **6 morsures** adversariales,
reverts par `cp` ciblé (jamais `git checkout` large — cf. incident consigné) : **M1** DoD
13→14 (mord) · **M2** périmètre 13→99 (mord) · **M3** constat total 13→12 (mord) · **M4**
ligne DoD supprimée ⇒ INTROUVABLE · **M5 no-false-positive** : muter le « 12 » analytique
(12→11), total intact ⇒ gate **exit 0** (preuve qu'on ne gate que le total) · **M6
SILENT-GREEN** : dérive de la SOURCE (`git rm --cached` d'une fiche ⇒ `n_agents`=12), docs
figés à 13 ⇒ les **5 surfaces head-docs+GAP mordent** « dit 13 MAIS 03_agents/ en compte 12 »
(preuve que l'autorité est le git-index, pas la cohérence interne). Arbre restauré. **7 gates
re-joués → exit 0** : check_ci_integrity · check_artifacts · check_docs · check_readme_claims ·
check_regression · guard_constraints · validate_json. Aucune commande touchant au VPS.

## Sprint 8 · buffer · `roles_couverts` (« 44 ») — enquête anti-dérive → **non-surface prouvée** + documentation du garde-fou mono-source (évite un gate N+1 redondant #5)

**Contexte** — Série anti-dérive Sprint 8. Un balayage des comptes numériques a
exhumé un candidat de **cohérence croisée** : le total « **44 rôles couverts par les
5 portails** » apparaît, identique, dans **trois** manifestes byte-gatés
(`frontend/portails`, `frontend/chat_otoia`, `mobile/app_config` ·
`counts.roles_couverts=44`). Chaque « 44 » vif n'est ancré qu'à SON propre manifeste
(mobile README · `demo/scenarios/out/run_sheet.md`). Aucun `ci/*.sh` ne compare les
trois entre eux. Hypothèse initiale : surface non couverte, analogue au cross-check
RBAC « 50 » (`rbac/apply_plan/rbac_apply_plan.py:149` exige
`fixtures==userperm==roleprofile`).

**Enquête — l'analogie RBAC-50 ne tient PAS** (mémoire *fix-vs-gate-transitively-
protected-constant*). Le « 50 » EST cross-gaté car ses trois rendus **re-dérivent**
chacun leur décompte avec leur **propre** logique. Ici, au contraire, les trois
modules **importent la même** fonction de partition : `mobilelib/deps.py:77` et
`chatlib/deps.py:84` font tous deux `roles_by_portail = portails_builder.roles_by_portail`
(unique définition `frontend/portails/wslib/builder.py:36`), appliquée à l'**unique**
contrat `rbac/rbac_50_roles.json` (une seule copie de chaque dans `git ls-files`).
`roles_couverts` est donc **transitivement mono-sourcé** (code partagé + donnée
partagée) : les trois totaux **ne peuvent diverger** que si cette fonction ou ce
contrat change — auquel cas les trois changent ensemble et `check_artifacts`
(reproductibilité byte-à-byte) vérifie chaque rebuild. Un gate « 44==44==44 » serait
**redondant (#5)** et masquerait la vraie garantie (le partage de code), pas une
surface à protéger.

**Livraison — DOCUMENTATION du garde-fou, pas un gate.** Ajout d'un bloc sourcé
« **Non-surface documentée — `roles_couverts` N'EST PAS cross-gaté, à dessein** » dans
`ci/README.md` (entre le bloc mobile Expo et le bloc CONFOTUR) : cite les 3 imports
partagés + le contrat unique + contraste explicite avec le cross-check RBAC-50, pour
qu'une session future ne rajoute PAS le gate redondant. Chemins en backticks (non
link-checkés, cf. mémoire *backtick-path-escapes-check-docs*).

**Vérif** — 4 citations de lignes vérifiées exactes (`deps.py:77`, `deps.py:84`,
`wslib/builder.py:36`, `rbac_apply_plan.py:149`). **7 gates re-joués → exit 0** :
guard_constraints · validate_json · check_docs · check_artifacts · check_regression ·
check_ci_integrity · check_readme_claims. Aucune commande touchant au VPS.

## Sprint 8 · buffer · Reporting stakeholder (canal 2) — rattrapage du snapshot `daily_reports/2026-08-02` sur les 2 sessions `run_ci.sh` (commits `49123ec`/`dcb463e`)

**Contexte** — Série buffer Sprint 8. Le canal 2 (instantané stakeholder
`daily_reports/`, mémoire *two-logging-channels*) avait été rattrapé à 14:35 (addendum
`143555`) jusqu'au commit `1c0cb8e`. Depuis, **2 sessions buffer** ont introduit l'outil
DevOps `run_ci.sh` (`49123ec` @15:06 · `dcb463e` @15:36) sans mise à jour du canal 2 :
l'instantané avait de nouveau **glissé de 2 sessions**, alors que sa trace fine existait
déjà au canal 1 (ce fichier).

**Livraison — 3ᵉ addendum ADDITIF** dans `05_deliverables_mvp/daily_reports/2026-08-02.md`
(corps + 2 addenda intacts). Consolide les 2 sessions : (1) `49123ec` — `run_ci.sh`,
miroir LOCAL du gate dérivé de `gate.needs` (30 jobs = 7 gates `ci/*.sh` + 23 suites
module · PAS un gate #5 · root ∉ `ci/*.sh`) ; (2) `dcb463e` — retrait du décompte figé
30/23/7 du commentaire, désormais DÉRIVÉ à l'exécution. Aucun nouveau module (contrainte
respectée) · aucun gate ajouté (#5) · le seul livrable net est l'outil DevOps déjà gaté
par construction.

**Anti-invention #6** — chaque chiffre du tableau d'état re-dérivé par recompute `python3`
indépendant sur les artefacts commités : **22/22** qualité (scores distincts = {100} ·
verdict `PASS`) · **22 suites / 564 ran / 564 passés / 0 échec / 0 erreur / 0 skip**
(`regression_run.json` · `PASS`) · **15/15** promesses `in_repo` (`acceptance_matrix.json`
· verdict `true`) · **13** fiches (`03_agents/`) · `gate.needs` = **30**. Zéro donnée
nouvelle introduite ; l'état launch-readiness est **inchangé et vert**.

**Vérif** — `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate) ·
`check_docs.sh` → conforme (les liens relatifs du nouvel addendum — `../../run_ci.sh`,
`../../.gitea/workflows/ci.yml`, `../qa/...` — mirrorent le style des addenda existants et
passent le link-check). Arbre `git status --porcelain` vide avant l'édition. Aucune
commande touchant au VPS.

## Session 20260802_163601 · Sprint 8 · buffer · Fiche `devops/AGENT.md` : documentation du livrable `run_ci.sh` (angle mort de complétude)
- **Constat** : `run_ci.sh` (miroir local du gate, ajouté aux commits `49123ec`/`dcb463e`) est déjà indexé dans la table Navigation du `README.md` racine, mais **absent de la fiche de son agent propriétaire** (`03_agents/devops/AGENT.md`) — la table « Livrables DevOps réellement produits » n'en listait que 2 (ci.yml+ci/, deploy_runbook). Gap de **complétude doc-vs-réalité**, pas une dérive de chiffre.
- **Action** : ajout d'une 3e ligne à la table + sous-section dédiée décrivant `run_ci.sh` **avec exactitude** : outillage local (confort dev), **PAS un gate** (CLAUDE.md #5 · aucun `ci/*.sh`, hors `gate.needs`, racine ∉ `ci/` donc hors invariant INV-B), liste de checks **dérivée** de `gate.needs` (zéro liste en dur → zéro dérive), décompte imprimé à l'exécution (#6). Aligné sur la mémoire `local-ci-runner`.
- **Zéro nouveau module · zéro gate ajouté** (#5). Seul changement = prose de fiche.
- **Vérif** : `check_docs.sh` valide le nouveau lien `../../run_ci.sh` (HARD) → vert · `guard_constraints.sh` vert · `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**.

## Session `20260802_170601` · Sprint 8 · buffer · DevOps — `run_ci.sh` : `-h|--help` + en-tête `--static` honnête (UX du lanceur, PAS un gate #5)

**Contexte** — Série buffer Sprint 8, tout vert (`run_ci.sh` → 30 PASS). L'outil
`run_ci.sh` (miroir local du gate, mémoire `local-ci-runner`) était fonctionnellement
complet mais avait **2 vraies aspérités d'UX**, reproduites avant correction :
1. `run_ci.sh --help` (et `-h`) échouait en « option inconnue » + exit 2, alors qu'un
   bloc d'usage riche dormait dans l'en-tête du script, jamais surfacé — la convention
   `--help` la plus universelle manquait.
2. En mode `--static`, l'en-tête annonçait toujours « **30 jobs = 7 gates + 23 suites** »
   alors que seuls les 7 gates s'exécutent → le récap final « **7 PASS** » **contredisait**
   l'en-tête (vert honnête mais message trompeur).

**Action** (édition de l'outil `run_ci.sh` uniquement + accord doc) :
- Ajout d'une fonction `usage()` (heredoc) et branchement `-h|--help` **AVANT** la
  résolution git → l'aide marche même hors checkout (vérifié : exit 0 depuis `/tmp`),
  alors qu'un vrai run hors git reste exit 3. Codes de sortie documentés (0/1/2/3).
- En-tête d'exécution rendu **mode-conscient** : en `--static`, affiche
  « 30 jobs · mode --static → 7 gates ci/*.sh seuls (23 suites ignorées) » — plus de
  contradiction avec le récap. Le mode plein est inchangé.
- Message d'option inconnue et bloc d'usage `ci/README.md` mis à jour (ajout `--help`).
- **Toutes les valeurs restent DÉRIVÉES** de `gate.needs` (`n_jobs/n_gates/n_suites`) —
  aucune liste ni compte figé introduit (#6). **Zéro nouveau module · zéro gate ajouté**
  (#5) : `run_ci.sh` demeure un lanceur (racine ∉ `ci/*.sh`, hors `gate.needs`, INV-B).

**Vérif** — `--help`/`-h` → exit 0 (dans + hors git) · `--bogus` → exit 2 · en-tête
`--static` désormais « 7 gates seuls » cohérent avec « 7 PASS » · `--list` inchangé
(30 jobs) · `check_docs` · `check_readme_claims` · `guard_constraints` verts ·
`run_ci.sh` (plein) → **30 PASS · 0 FAIL · 0 SKIP**. Arbre propre avant édition.
Aucune commande touchant au VPS.

## Session `20260802_190604` · Sprint 8 · buffer · QA — vérification launch-readiness : mutation-test que les 7 gates statiques MORDENT encore (7/7 RED, revert propre)

**Contexte.** Repo 100 % construit (`run_ci.sh` → 30 PASS · 0 FAIL) ; sprints 1→7
livrés côté dépôt, phase buffer Sprint 8. Un audit doc-vs-réalité frais (top-level
docs + 13 fiches `03_agents/*/AGENT.md` + `daily_reports` + roadmap) n'a trouvé
**aucune dérive non gatée** — la surface driftable évidente est épuisée. Décision
(#5 — ne pas empiler un gate N+1 redondant) : plutôt qu'inventer du travail, exécuter
le livrable QA propre de Sprint 8 (« **Regression tests exhaustifs** ») sous l'angle
le plus fort : **prouver que les gates existants attrapent réellement une régression**
(un gate vert qui ne mord pas est pire qu'absent — cf. mémoire `verify-uncovered-before-gating`
+ `guard-constraints-flags-usage-not-mention`).

**Méthode.** Pour chacun des 7 gates statiques `ci/*.sh`, injecter une mutation
ciblée de la classe de dérive qu'il est censé traquer, exécuter le gate, exiger un
**exit non-zéro** (RED), puis **revert par chemin ciblé** (jamais `git clean` — #interdits)
et re-vérifier l'arbre à 0 fichier modifié.

| # | Gate | Mutation injectée | Résultat |
|---|------|-------------------|----------|
| 1 | `guard_constraints` | URL interdite `https://github.com/…` ajoutée à `README.md` | **RED** ✓ (`Interdit détecté — GitHub`) |
| 2 | `check_ci_integrity` | retrait de `constraints-guard` de `gate.needs` (ci.yml) | **RED** ✓ (`ABSENT de gate.needs` + INV-B) |
| 3 | `check_docs` | lien Markdown interne cassé `./this/does/not/exist.md` | **RED** ✓ (`lien cassé`) |
| 4 | `check_readme_claims` | compte fiches `13`→`14` dans `README.md` | **RED** ✓ (`README dit 14 MAIS artefact dit 13`) |
| 5 | `check_artifacts` | octet parasite ajouté à `crm/commissions/out/MANIFEST.json` | **RED** ✓ (`DÉRIVE … diffère du build frais`) |
| 6 | `validate_json` | JSON syntaxiquement invalide dans un artefact commité | **RED** ✓ (exit 1) |
| 7 | `check_regression` | `totals.passed` `564`→`565` dans `out/regression_run.json` | **RED** ✓ (`DÉRIVE : le run frais diffère du commité`) |

**Deux « faux verts » — analysés, PAS des trous de couverture :**
- Mutation d'un `MANIFEST.json` de régression testée d'abord contre `check_regression`
  → **vert** : normal, `check_regression` ne possède QUE `regression_run.json`
  (artefact de `run`, compteurs exécutés) ; le `MANIFEST.json` (artefact de `build`)
  est possédé par `check_artifacts` — mutation re-testée contre lui → **RED** ✓.
  **La frontière de propriété entre les deux gates est nette et correcte.**
- Première tentative de mutation de `regression_run.json` inefficace (bump d'un `int`
  de premier niveau — or les compteurs sont **imbriqués** sous `totals`, aucun `int`
  racine) → rewrite byte-identique → vert légitime. Corrigée en mutant `totals.passed`
  (ligne 7 ci-dessus) → **RED** ✓.

**Conclusion.** **7/7 gates statiques MORDENT** sur leur classe de dérive respective,
frontières de propriété inter-gates confirmées, **aucun trou de couverture**. Le
verrou anti-régression de la plateforme est prouvé opérationnel pour le launch
(pas seulement « vert par chance »). **Zéro nouveau module · zéro gate ajouté (#5) ·
zéro fichier de production modifié** — travail de vérification pur, chaque mutation
revert par chemin ciblé. Arbre final : `git status --short` → 0 ligne · `run_ci.sh`
→ **30 PASS · 0 FAIL · 0 SKIP**. Aucune commande touchant au VPS.

---

## Session `20260802_200607` · Sprint 8 buffer · reporting canal 2 + audit doc-vs-réalité

**Contexte.** Relu CLAUDE.md, la roadmap (Sprint 8 · *Launch + Buffer*), MEMORY.md et le
journal du jour. Tous les sprints 1→7 livrés côté repo ; `run_ci.sh` → **30 PASS · 0 FAIL ·
0 SKIP** en entrée de session. Surface driftable évidente épuisée (fiches 13/13 closes,
gates saturés + les 7 statiques prouvés mordants en `190604`).

**Décision (workflow #5 — ne pas empiler de gate redondant).** Plutôt qu'inventer un module
ou un gate N+1, exécuter les deux tâches de veille légitimes du buffer saturé :

1. **Audit doc-vs-réalité (classe *prose-fact* non gatée).** Recompute `python3` indépendant
   des agrégats + sweep des faits-prose sur README racine, `ci/README.md`,
   `AGENTS_EXISTING_ASSETS.md`, `PORTAIL_BANCABLES_4BIG.md`, 13 fiches `03_agents/*/AGENT.md`
   et les READMEs module. **Résultat : aucune dérive** (chaque fait spot-checké contre l'état
   courant est exact). Agrégats confirmés : **22/22** qualité (scores = {100}) · **564/564**
   régression (`totals` suites 22) · **15/15** recette (`in_repo`, verdict `True`) · **13**
   fiches · `gate.needs` = **30** (7 gates + 23 suites).

2. **Reporting stakeholder (canal 2).** Le snapshot `daily_reports/2026-08-02.md` avait
   lapsé depuis le 4ᵉ addendum (`180604`, 18:06). Rattrapage des **3 sessions intercalées**
   (`183604` `97d3b64`+`f954cc1` · `190604` `7c79e43` · `193605` `8a08065`) → **5ᵉ addendum
   ADDITIF** (corps + 4 addenda intacts), chaque chiffre re-sourcé par recompute indépendant.

**Vérifications.** `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** après édition ; `check_docs`,
`check_readme_claims`, `guard_constraints` verts (seuls fichiers touchés = docs de reporting,
hors périmètre des gates de code). **Zéro nouveau module · zéro gate ajouté (#5) · zéro
fichier de production modifié.** Aucune commande touchant au VPS (#8).

---

## Session `20260802_203609` · Sprint 8 buffer · audit prose-fact ciblé + résolution faux-positif confotur

**Contexte.** Relu CLAUDE.md, la roadmap (Sprint 8 · *Launch + Buffer*), MEMORY.md et le
journal du jour. Entrée de session : `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** ;
arbre `git status --short` → 0 ligne. Tous sprints 1→7 livrés. Aucun TODO/placeholder réel
(les `placeholder` restants sont tous du design anti-invention #6, pas des trous).

**Décision (workflow #5 — ne pas empiler de gate/module).** Plutôt qu'un 6ᵉ addendum de
reporting (canal 2 déjà à jour au `200607`), cibler la **classe *prose-fact* NON gatée** :
faits libres (non-numériques) dans les README modules que CI ne vérifie pas. Fan-out lecture
(agent Explore) sur tous les `05_deliverables_mvp/*/README.md` + sous-README, chaque
affirmation factuelle spot-checkée contre le `.py`/`.json` du même module.

**Résultat : aucune dérive prose-fact.** Un seul candidat remonté par l'agent —
`legal/confotur/README.md:58` « 44 tests (dont **8 négatifs**) » — **résolu en FAUX-POSITIF**
avec preuve :
- `TestNegative` (tests/test_confotur.py 242–296) = **exactement 8** `test_reject_*` → « 8 négatifs » exact.
- `TestBuilderGuards` (297–324, 3 `assertRaises`) est une **classe distincte** (guard/raises),
  pas des « négatifs » ; l'agent les avait agrégés à tort (→ 11). Total `def test_` = **44** ✓.
- Même README « 14 invariants » (56/82) confirmé : **14** entrées `# INVn ·` dans
  `confotur_application_gen.py`, alignées sur ses 4 auto-phrasings (compte CONFOTUR déjà gaté).

**Trace mémoire.** Nouveau guard-fou `confotur-8-negatifs-not-drift` (genre
`fiche-accuracy-audit-closed` / `otov7-platform-not-drift`) : un audit test-count naïf
re-flaggera « 8 négatifs » → vérifier les frontières de classe avant de « corriger ».

**Vérifications.** `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**. **Zéro nouveau module ·
zéro gate ajouté (#5) · zéro fichier de production modifié** — session de vérification pure
(seul écrit repo = ce journal). Aucune commande touchant au VPS (#8).

---

## Session 20260802_210609 · Sprint 8 · buffer · Dérive doc-vs-CODE réelle corrigée (docstring de production)

**Contexte.** État stationnaire : `run_ci.sh` = 30/30 vert, roadmap S1–S8 livrée et
gatée, reliquat roadmap (pipelines BIM/IFC/rendu Blender · déploiement VPS · soumissions
stores) hors périmètre sandbox. Plutôt qu'un énième audit no-op, chasse ciblée d'un défaut
**réel** dans un module de parsing (là où les cas limites mordent).

**Trouvaille (classe NOUVELLE, distincte des dérives README gatées).** Le docstring
d'en-tête de `05_deliverables_mvp/publiciste/lib/parser.py` (bloc « Sources lues », l. 26)
annonçait :

> `40_llm_outputs/commercial.md → positionnement FR (fallback prix §5.3)`

Or **aucun fallback de prix depuis `commercial.md` n'existe dans le code** : `commercial.md`
n'alimente QUE `parse_positionnement_fr` (l. 275+, « 1er paragraphe non-titre »). Preuve :
`grep -rniE "5\.3|fallback.*prix|prix.*fallback|commercial.*prix" lib/ publiciste.py` →
**une seule** occurrence, le docstring lui-même. Le docstring décrivait donc un
**comportement inventé qui n'a jamais été implémenté** — exactement l'anti-pattern que
CLAUDE.md #6 (zéro invention) proscrit, transposé du côté documentation-du-code.

**Correction.** Reformulé en `→ positionnement FR (1er paragraphe non-titre)`, aligné mot
pour mot sur la logique réelle de `parse_positionnement_fr`. **Docstring seul — zéro
changement de logique, zéro changement de signature, zéro test touché.**

**Distinction de classe.** Ce n'est PAS une dérive de compte gatée (check_readme_claims)
ni une dérive de prose README (check_docs) : c'est une inexactitude **dans le docstring du
module de production**, surface non couverte par les gates existants et distincte des
classes déjà cataloguées. Vérifié qu'aucun sibling (`README.md` publiciste, `AGENT.md`
publiciste) ne répète la fausse allégation « fallback prix » → correction ponctuelle,
source unique.

**Vérifications.** `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (inchangé). **Zéro
nouveau module · zéro gate ajouté (#5) · un seul fichier de production touché (docstring)**.
Aucune commande VPS (#8). Commit `a2c2e0b`.

---

## Session 20260802_213610 · Sprint 8 · buffer · Dérive doc-vs-CODE réelle : docstring demo_scenario omettait un output produit

**Contexte.** État stationnaire (`run_ci.sh` = 30/30 vert, roadmap S1–S8 livrée et
gatée, reliquat hors périmètre sandbox). Poursuite de la classe de dérive découverte
la session précédente (docstring de production vs code réel — surface non gatée, distincte
des gates README/compte). Fan-out de 3 audits read-only (Explore) sur les ~22 modules à
docstring dense, chacun vérifiant CHAQUE allégation factuelle du docstring d'en-tête contre
le code.

**3 candidats remontés · 2 réfutés par vérification indépendante · 1 réel corrigé.**

**RÉFUTÉ (faux-positif de compte).** `fiscal/ecf_dgii/ecf_dgii_gen.py` l.110 « 12 invariants » :
l'agent alléguait « la numérotation commence à 2 → pas d'invariant 1 ». FAUX — l'invariant 1
EST la conformité au schéma (`errors = list(maison.validate(bundle, schema))`, l.112) et
`12a`/`12b` sont les deux moitiés de l'invariant 12 (unicité + comptes/tipos). Décompte réel :
1(schéma)+2..11+12(a,b) = **12 exactement**, aligné mot-pour-mot sur le ledger README « Les 12
invariants » + help CLI + tests + message stdout. Piège de compte classique (cf. mémoire
`invariant-ledger-count-gate`, `confotur-8-negatifs-not-drift`). Zéro changement.

**RÉFUTÉ implicite.** Cluster RBAC/legal/bancable (7 modules) : aucune dérive, tous les
inputs/outputs/compteurs/comportements des docstrings exacts.

**RÉEL — corrigé.** `demo/scenarios/demo_scenario_gen.py`. Le générateur écrit **TROIS**
fichiers — `run_sheet.json` (l.196), `MANIFEST.json` (l.196) ET `run_sheet.md` (l.202-203,
le « prompteur » Markdown, annoncé jusque dans le message stdout l.206 « + prompteur
run_sheet.md »). Or la documentation DANS le module n'en listait que DEUX à trois endroits :
le diagramme de chaîne de valeur du docstring (l.19), le résumé sous-commande du docstring
(l.31) et la `help=` de l'argparse `build` (l.236). Le `run_sheet.md` a été ajouté au commit
`aebe9b7` (« prompteur Markdown ») qui a MIS À JOUR le README (qui, lui, documente
correctement les 3 sorties, l.27-29/76-78) mais a laissé le docstring/help du .py en arrière.
Dérive doc-vs-CODE réelle, non gatée (check_docs/check_readme_claims ne couvrent pas la prose
intra-module), même classe que le fix publiciste parser de la session 210609.

**Correction.** Les 3 emplacements alignés sur les 3 sorties réelles :
diagramme → `out/run_sheet.json + out/run_sheet.md + out/MANIFEST.json` ;
résumé sous-commande + help argparse → `out/run_sheet.{json,md} + out/MANIFEST.json`.
**Docstring/help seuls — zéro changement de logique, de signature ou de test.**

**Vérifications.** `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (inchangé). Build réel
re-exécuté en dossier temp → émet bien `run_sheet.json`, `run_sheet.md`, `MANIFEST.json`
(3 fichiers = la doc corrigée). **Zéro nouveau module · zéro gate ajouté (#5) · un seul
fichier de production touché (docstring + help)**. Aucune commande VPS (#8).

---

## Session `20260802_220611` — Rattrapage canal 2 + sweep doc-vs-CODE sorties générateurs

**Contexte.** Reprise du buffer Sprint 8 en veille saturée. CI vert d'entrée
(`./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**). Roadmap 8-semaines intégralement
livrée ; toutes les files de travail buffer (fiches 13/13 closes, 7 gates statiques
prouvés mordants, audit doc-vs-réalité) à jour aux sessions précédentes.

**Analyse — recherche d'une dérive doc-vs-CODE réelle (classe RÉCURRENTE, non close).**
Sweep outil-assisté sur les **22 générateurs** (`*_gen.py` + `lib/generator.py` +
`seolib/builder.py`) : extraction des fichiers réellement écrits (`os.path.join(out, "…")`)
comparés aux sorties commitées dans chaque `out/`, croisée avec les déclarations de
docstring/help. **Résultat : concordance totale.** Les deux modules corrigés plus tôt
aujourd'hui sont propres — `demo/scenarios/demo_scenario_gen.py` émet bien ses **3**
sorties (`run_sheet.json` + `run_sheet.md` + `MANIFEST.json`), toutes commitées et
désormais fidèlement documentées ; `publiciste/lib/generator.py` sans « fallback prix »
fantôme résiduel. **Aucune nouvelle dérive de la classe déclaration-de-sortie.** Les
faux-positifs de mon regex (fichiers `_spec.json`/`.schema.json` en « written ») sont des
**entrées lues**, pas des sorties — vérifiés manuellement, non-dérives.

**Réalisé — reporting canal 2 (tâche de veille #1).** L'instantané stakeholder
[`daily_reports/2026-08-02.md`](../05_deliverables_mvp/daily_reports/2026-08-02.md) avait
**lapsé de 4 sessions** depuis sa dernière mise à jour (`200607` / commit `0e76411`).
Ajout d'un **6ᵉ addendum ADDITIF** (corps + 5 addenda intacts) consolidant les commits
`0e76411` · `b3f131e` · `a2c2e0b` · `12c784c` · `eae3787`, dont **les deux corrections
d'honnêteté doc-vs-CODE réelles** de la journée (docstring publiciste « fallback prix §5.3 »
fantôme ; docstring/help `demo_scenario_gen` omettant `run_sheet.md`). Chaque agrégat
**re-sourcé par recompute `python3` indépendant** contre l'artefact commité (anti-invention
#6) : **22/22** qualité (scores distincts = {100}) · **564/564** régression
(`totals` 22 suites · 564 ran · 0/0/0) · **15/15** recette (`in_repo`, verdict `True`) ·
**13** fiches · `gate.needs` = **30** (7 gates + 23 suites).

**Vérifications.** `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (inchangé après édition,
seuls des fichiers de doc/log touchés). **Zéro nouveau module · zéro gate ajouté (#5) ·
zéro fichier de production modifié** (reporting + vérification pure). Aucune commande
touchant au VPS (#8) · aucun `git clean` (#interdits).

---

## Session `20260802_223614` — Dérive doc-vs-CODE réelle : docstring `q4lib/builder.py` « 17 modules » périmé (17 → 22)

**Contexte.** Reprise buffer Sprint 8, veille saturée. CI vert d'entrée
(`./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**). Roadmap intégralement livrée.

**Analyse — sweep docstring-vs-CODE des `lib/` submodules (classe RÉCURRENTE, non close).**
Les sessions précédentes avaient balayé les **déclarations de sortie** des 22 générateurs
de tête. Nouveau périmètre ce jour : les **docstrings de module** des sous-paquets `lib/`
(là où la vraie dérive du parser publiciste vivait). Extraction AST de tous les docstrings
mentionnant des I/O (Sources lues / Entrées / Sortie / fallback / §), confrontés au code.
**Résultat : concordance quasi totale** — parser publiciste propre (6 sources lues = 6 lues
dans `parse_projet`/`_sources_presentes`), tous les builders/deps/frappe fidèles.

**UNE dérive réelle trouvée.** Docstring `qa/audit_4big/q4lib/builder.py` l.4 :
« (b) les **17** modules atteignent ≥ 95/100 ». Or l'artefact commité
`out/quality_report.json` porte `totals.modules = **22**` · `coverage.ci_modules_count = 22`
· `pass = 22`. **Preuve d'antériorité de la dérive :** le gate `ci/check_readme_claims.sh`
l.414-417 la documente noir sur blanc — « 17/17 modules SAISI À LA MAIN … PÉRIMÉ en silence
quand des modules ont été ajoutés (17 → 22) ». Le gate **recalcule** le compte du *README*
de l'auditeur (désormais « 22/22 » l.68) — mais le **docstring du module** est resté hors
couverture (les docstrings ne sont ni link- ni count-checkés · cf. mémoire
`docstring-vs-code-drift`). Le même « 17 » gaté dans le README avait survécu, non gaté, dans
le docstring de production.

**Correction — honnête ET anti-re-dérive (pas de substitution d'un compte magique).**
Le verdict réel du builder est `PASS ssi coverage.ok AND not below` — le nombre de modules
est **DÉRIVÉ** (`len(modules)`), jamais figé. Remplacer « 17 » par « 22 » ne ferait que
re-dériver au module #23. Reformulé en énonçant la **RÈGLE** : « TOUS les modules audités
atteignent ≥ 95/100 (aucun sous le seuil) », + note explicite que le compte est dérivé
(`len(modules)` == `totals.modules`, 17 → 22 à ce jour) et ne se retouche pas ici. Aligné
sur l'éthos `fix-vs-gate` (ne pas empiler un gate N+1 ; décrire depuis l'autorité = le code).

**Vérifications.** `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (inchangé — docstring seul,
zéro changement de logique). README module gaté déjà à **22/22** (concordant). **Zéro nouveau
module · zéro gate ajouté (#5) · un seul fichier de production touché (docstring)**. Aucune
commande VPS (#8) · aucun `git clean` <!-- ci-allow : mention de l'interdit absolu, jamais exécuté -->.

---

## Session 230619 · Régression `constraints-guard` RED réparée (log-prose auto-tripping)

**Contexte.** `./run_ci.sh` au démarrage : **29 PASS · 1 FAIL** — job `constraints-guard`
RED. Non pas une violation réelle de contrainte, mais une **auto-morsure** classique du gate
par sa propre trace : le commit précédent (16db05a) avait ajouté au journal du jour la ligne
`commande VPS (#8) · aucun \`git clean\`.` — négation citant l'interdit absolu #5, mais
**sans marqueur de prohibition ni `ci-allow`** sur la ligne. Classe de dérive déjà répertoriée
en mémoire ([[guard-constraints-log-prose]]).

**Diagnostic.** La ligne voisine 2080 (`… aucun \`git clean\` (#interdits).`) ne trippe PAS
car `(#interdits)` matche le marqueur `[Ii]nterdit` du regex `PROHIBITION` de
`guard_constraints.sh:25`. La ligne 2119 se terminait par « aucun \`git clean\`. » <!-- ci-allow --> — aucun
des marqueurs (`JAMAIS`/`❌`/`pas de`/`interdit`/`ci-allow`…) présent → flaggée.

**Correctif.** Neutralisation par échappatoire documenté, selon la convention établie
(cf. 2026-08-01.md:1689) : ajout de `<!-- ci-allow : mention de l'interdit absolu, jamais
exécuté -->` sur la ligne. **Un seul fichier touché (journal), une seule ligne. Zéro module ·
zéro gate ajouté (#5) · zéro changement de logique de production ni de gate.**

**Vérifications.** `bash ci/guard_constraints.sh` → **VERT** (`✅ Contraintes
NON-NÉGOCIABLES respectées`). `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (gate complet
re-vert). Aucune commande touchant au VPS (#8). Aucun `git clean` <!-- ci-allow : mention de l'interdit, jamais exécuté -->.

---

## Session 233620 · Sweep docstring-vs-CODE élargi (62 fichiers) + reporting canal 2 (7ᵉ addendum)

**Contexte.** `./run_ci.sh` au démarrage : **30 PASS · 0 FAIL · 0 SKIP** — gate vert. Sprint 8
buffer en veille saturée (fiches 13/13 closes, gates statiques 7/7 mordants). Tâche de veille
#1 des addenda précédents : tenir le canal 2 (`daily_reports/`) à jour à chaque session +
poursuivre l'audit doc-vs-CODE de la classe *docstring-drift* (RÉCURRENTE, non gatée,
`docstring-vs-code-drift`).

**Audit exhaustif à couverture élargie.** Les sessions précédentes n'auditaient la classe
docstring-vs-CODE que sur les **entrypoints** (`*_gen.py`). Extension cette session aux
**modules de bibliothèque** — surface où la dernière dérive réelle du jour était apparue
(`q4lib/builder.py` « 17 modules », commit `16db05a`). Confrontation docstring (module +
fonction) vs code réel (sorties écrites, entrées lues, fallbacks, règles, compteurs) sur :
- **22 générateurs** (entrypoints) — sorties déclarées vs `open/write/dump` réels.
- **40 modules `*lib/`** (builders, scorers, parsers, finance, criteria, controls, runner,
  ncf, aggregator, renderer, model…) — inputs/fallbacks/règles/compteurs déclarés vs code.

**Résultat : 62/62 fichiers de production CONCORDANTS · aucune dérive résiduelle.** Y compris
les deux modules corrigés ce jour (`demo_scenario_gen` 3 sorties, `q4lib/builder.py` compte
dérivé). La surface docstring-drift est **saturée à ce jour** — signal fort de couverture, à
re-scanner après toute évolution de code (n'est PAS une classe close par nature). Audit pur —
**zéro fichier de production modifié cette session**.

**Reporting canal 2 (7ᵉ addendum).** Le stakeholder snapshot `daily_reports/2026-08-02.md`
s'arrêtait au 6ᵉ addendum (`220611`, 22:06). Rattrapage additif des **2 sessions intercalées**
(`223614`/`16db05a` docstring 17→22 · `230619`/`56a8802` gate constraints-guard RED réparé),
chaque agrégat de l'état MVP **re-vérifié par recompute `python3` indépendant** contre
l'artefact commité (anti-invention #6) : **22/22** qualité (scores = {100}) · **564/564**
régression (22 suites, 0/0/0) · **15/15** recette (`in_repo`, verdict `True`) · `gate.needs` =
**30** (7 gates + 23 suites). Corps et six addenda antérieurs **intacts** (convention additive).

**Vérifications.** `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate,
inchangé — reporting seul, zéro logique). Recompute indépendant concordant sur les 4
dimensions. Arbre `git status` propre avant édition. **Zéro nouveau module · zéro gate ajouté
(#5) · aucune logique de prod ni de gate touchée**. Aucune commande VPS (#8) · aucun
`git clean` <!-- ci-allow : mention de l'interdit absolu, jamais exécuté -->.
