Files
oto-enterprise-os-dtp/05_activity_log/2026-08-02.md
T
Claude Code DTP Worker 7c79e4302a [DTP-Worker 20260802_190604] Sprint 8 · buffer · QA launch-readiness : mutation-test que les 7 gates statiques MORDENT (7/7 RED)
Livrable QA 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). Audit doc-vs-réalité frais = aucune dérive
non gatée → au lieu d'empiler un gate N+1 (#5), vérification pure.

7/7 gates statiques ci/*.sh RED sur leur classe de dérive respective :
guard_constraints (URL github interdite), check_ci_integrity (job retiré de
gate.needs), check_docs (lien interne cassé), check_readme_claims (compte 13→14),
check_artifacts (octet parasite MANIFEST), validate_json (JSON invalide),
check_regression (totals.passed 564→565). Chaque mutation revert par chemin
ciblé (jamais git clean).

2 « faux verts » analysés = PAS des trous : frontière de propriété nette entre
check_regression (regression_run.json/run) et check_artifacts (MANIFEST/build) ;
1re mutation regression_run.json inefficace (compteurs imbriqués sous totals).

Zéro nouveau module · zéro gate ajouté (#5) · zéro fichier de production modifié.
Détail : 05_activity_log/2026-08-02.md. run_ci.sh 30 PASS · arbre propre.

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

1859 lines
128 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Activity log · 2026-08-02 · Claude Code DTP Worker
## 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.