# Activity Log · 2026-08-05 · Claude Code DTP Worker

## Session `20260805_234409` — VÉRIF adversariale ciblée : le candidat « MANIFEST.json manquant » de la fiche frontend_console est un NON-défaut par design (les 2 lignes l'omettent identiquement) · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée ; le canal de currency
`daily_reports/2026-08-05.md` est déjà au HEAD effectif (`8903a4b`, porté par `21f55a9`). Plutôt
que répéter le motif récent de mise à jour de currency (churn), lancement d'une **chasse
adversariale à UN vrai défaut ungaté** (agent `Explore` skeptique) sur les surfaces sous-balayées :
prose-FACT vs artefacts · docstring↔code des `*_gen.py`/`*lib/` · liens in-repo cassés.

**Candidat remonté puis RÉFUTÉ.** L'agent a signalé `03_agents/frontend_console/AGENT.md:29`
(cellule « Sortie » du chat_otoia = `out/{custom_block,chat_mount}.json`, **2** fichiers) comme
omettant le 3ᵉ fichier écrit par le code — `chat_otoia_gen.py:227-229` écrit bien
`custom_block.json` / `chat_mount.json` / **`MANIFEST.json`**. **Vérification indépendante → NON-défaut :**
la ligne **jumelle** portails (`AGENT.md:28`, cellule = `out/workspace.json`) omet **elle aussi**
`MANIFEST.json`, alors que `workspaces_gen.py:218-219` l'écrit. Les **deux** lignes suivent donc la
même **convention éditoriale** : la cellule « Sortie » liste la charge utile (*payload*), pas le
manifeste de traçabilité. L'énumération **complète** des 3 fichiers est bien présente et correcte
là où elle doit l'être — docstring du générateur (`chat_otoia_gen.py:12/15/18`) + README de module.
« Corriger » la seule ligne chat créerait une **asymétrie** ; éditer les deux churnerait une cellule
**parsée par un gate** (`ci/check_readme_claims.sh`, `CO_FI` ~L3506/L3576 en extrait les « TROIS
attributs data-derived » + comptes tests/invariants). Décision : **CLEAN, laisser tel quel** (#5/#6,
motif « X not drift, don't fix »).

**Mémoire projet.** Nouveau fichier `fiche-sortie-cell-omits-manifest-by-design.md` (+ index)
pour qu'une session future ne re-remonte pas le faux positif.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**,
**0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `33` recompté de `run_ci.sh`),
**aucune commande VPS** (#8). Seule édition in-repo : ce journal.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.

---

## Session 224359 · FIX RÉEL — commentaire fantôme dans le gate `ci/validate_json.sh` (check de conformité `$schema` annoncé, jamais implémenté) · nouvelle sous-classe comment-vs-code sur les scripts de GATE eux-mêmes

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo ; les 24 modules du
`quality_report.json` (audit_4big) sont **tous à 100/PASS** (aucun < 95, contrainte #5) → la
frontière automatisable restante est soit un arbitrage produit (bloqué Michel, cf.
[[open-decisions-register]]), soit du déploiement VPS (hors périmètre #8). Bascule sur une
surface de dérive **non ré-auditée** : les **commentaires internes des scripts de gate**
`ci/*.sh` eux-mêmes — les sweeps docstring-vs-code antérieurs étaient scopés aux **générateurs
de production** + helpers `*lib/` (cf. [[docstring-vs-code-drift]]), jamais aux gates.

**Finding (réel, isolé).** `ci/validate_json.sh:27` portait un commentaire orphelin —
`# Si un $schema est déclaré, contrôle que 'draft' est reconnu (info seule).` — placé dans la
branche `then` du parse-loop, mais **aucun code** ne lit `$schema` ni ne contrôle le `draft` :
la branche n'imprime que le `✓`. Le gate valide la **bonne-formation SEULEMENT** (parse strict) ;
la conformité structurelle au `.schema.json` est l'oracle tiers OPTIONNEL `jsonschema` de la
suite régression (cf. [[regression-baseline-17-skips-by-design]]). Le commentaire promettait donc
un check inexistant — même classe honnêteté que #6 (rien ne doit affirmer un contrôle absent).

**Vérification d'isolement.** Sweep Explore des **8** scripts de gate + `lib.sh`
(`check_readme_claims.sh` 5600+ l., `check_docs`, `check_artifacts`, `check_regression`,
`check_ci_integrity`, `check_mobile_workflow`, `guard_constraints`, `lib.sh`) pour la même classe
(commentaire affirmant un comportement que le code n'implémente pas) → **0 autre dérive** ; tous
les autres commentaires sont synchrones avec un code qui les honore. Dérive **localisée** au seul
`validate_json.sh`.

**Fix (2 volets, honnête · 0 invention).** (1) Suppression de la ligne fantôme l.27 (la branche
`then` fait exactement ce que le code montre — imprimer le `✓`). (2) En-tête : ajout d'un bloc
**PÉRIMÈTRE** explicite disant que ce gate contrôle la bonne-formation SEULEMENT et que la
conformité `$schema` vit ailleurs (l'oracle `jsonschema` de `check_regression.sh`, 17 skips par
design) — le commentaire retiré devient une affirmation POSITIVE et vraie du périmètre, ce qui
prévient la réintroduction du fantôme par un futur lecteur.

**Portée / anti-churn.** **1 fichier de gate** édité (commentaire seul, **0 changement de logique
exécutable** — même AST bash), **0 artefact reconstruit**, **0 gate ajouté** (#5 — c'est un FIX de
dérive, pas un nouveau contrôle ; la classe est isolée, un gate serait inerte/redondant), **0
chiffre saisi à la main** (#6), **aucune commande VPS** (#8). `bash ci/validate_json.sh` = OK ·
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** avant et après (le fix ne touche aucun comportement
gaté). Seules éditions : `ci/validate_json.sh` + ce journal.

---

## Session 191333 · Audit de COMPLÉTUDE fiches AGENT.md (25 modules ↔ 13 fiches) — CLOSE CLEAN · réconciliation compte 22→25 · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo (0 checkbox ouverte).
Deux sondes préalables **négatives** : (a) Explore *very-thorough* de dérive prose/docstring sur
tout `05_deliverables_mvp/` (classes récurrentes docstring-vs-code + prose-facts-vs-numeric) =
**0 dérive ungatée** ; (b) relecture du méta-gate `ci/check_ci_integrity.sh` (INV-A `gate.needs`
== jobs non-manuels · INV-B tout `ci/*.sh` câblé/sourcé · INV-C `PYTHONUTF8`) = câblage exhaustif,
le risque « gate orphelin » que j'envisageais est **déjà fermé**. Surfaces de dérive usuelles
saturées (#5) → bascule sur un axe non ré-audité récemment : la **complétude** des fiches
(distincte de l'exactitude, cf. [[fiche-accuracy-audit-closed]]).

**Méthode.** Énumération des répertoires-modules RÉELS (`tests/` portant ≥1 `test*.py`) →
**25** modules. Le MEMORY portait « **22** modules livrés » : écart **réconcilié** — le split de
`rbac` en 4 sous-générateurs (`apply_plan` · `fixtures_gen` · `roleprofile_gen` · `userperm_gen`)
+ le `rbac` racine portent le total réel à **25** (pas 3 modules neufs non-fichés : simple
granularité de comptage). Cross-check de chacun contre les 13 `03_agents/*/AGENT.md`.

**Résultat — 25/25 dans ≥1 fiche.** 24 modules figurent comme **LIGNES de table** de leur fiche
propriétaire (colonnes `Module | Sprint | Rôle | Entrée CLI | Job CI | Tests`). Deux modules
n'ont **pas** de ligne de table dans la fiche `faisabilite` — vérifié **DÉLIBÉRÉ, pas un trou** :
- `pie/manifest` — documenté en **prose** (`03_agents/faisabilite/AGENT.md` §40-52) avec note
  explicite « les comptes exacts (groupes Master Data, règles de synchro, registre downstream)
  **font foi dans le README du module — cet agent en est la source, pas la copie** ». Une ligne
  de table à cellule *Tests* **réintroduirait un compte recopié** (#6, anti-invention) et
  **duperait la source** (#5). Laissé tel quel.
- `demo/scenarios` — sa ligne **source-unique** (auto-gatée) vit délibérément dans la fiche
  `crm` (§« Livrable transverse · Sprint 7 »), non redupliquée côté faisabilité (#5). La fiche
  faisabilité l'y renvoie explicitement (§31-38).

**Conclusion.** Complétude fiches **CLOSE** (confirme [[fiche-accuracy-audit-closed]], compteur
mémoire réaligné 22→25 · exemptions prose/source-unique documentées pour couper le re-chase).
Faux-positif « pie/manifest manque une ligne » **réfuté** à la source — même classe de FP que
le memory répète (paths deliverables-relatifs, illustrations spec-driven : *lus comme dérive,
n'en sont pas*). **0 fichier de production édité · 0 gate ajouté (#5) · 0 chiffre saisi à la
main (#6, tous recomptés) · aucune commande VPS (#8) · aucune plateforme git externe.**
Deliverable de session = ce log + màj memory. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.

## Session 184329 · CURRENCY canal stakeholder — **baseline régression corrigée** (607/17-skip) + 2ᵉ incident RED consigné · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` **intégralement livrée/gatée in-repo** (0 checkbox ouverte ;
15/15 `acceptance_matrix` `status=in_repo`). Plutôt qu'une énième sonde de dérive (surfaces usuelles
saturées · #5), j'ai vérifié la **currency du canal stakeholder** `daily_reports/2026-08-05.md`
(hors merge-gate : `check_docs`+`check_readme_claims` l'excluent explicitement) contre les artefacts
**courants** — dernière mise à jour du rapport = session `144304` (HEAD `f311a1b`), **5 commits
plus tôt**. Deux écarts **matériels** trouvés.

**Écart 1 — baseline régression : faux-vert `jsonschema` neutralisé (matériel).** Recompute
indépendant `python3` de `regression_run.json.totals` : **624 ran · 607 passed · 17 skipped · 0/0**,
verdict `PASS` — mais le rapport publiait « **624 passés · 0 skip** ». Cause : le refactor `9d30939`
lance chaque suite sous `python -S` (sans `site-packages`) reproduisant le runner Gitea pip-less →
l'oracle **optionnel** `jsonschema` (17 tests `skipUnless`, ~1/module) est **neutralisé partout**.
AVANT : `jsonschema` présent en local → `skipped=0` ; absent sur runner → `skipped=17` ⇒
`regression_run.json` commité = **FAUX-VERT** (vert local, ROUGE runner). APRÈS `-S` : matrice
**byte-déterministe** cross-environnement, baseline ancrée sur la couverture garantie (stdlib +
validateurs maison). Verdict inchangé `PASS`. La ligne « Régression » du canal se lit désormais
**607 passés · 17 skip (oracles optionnels)** ; les « 624 passés · 0 skip » antérieurs = snapshots
datés vrais à l'époque (*historical-repro = KEEP*).

**Écart 2 — 2ᵉ tree commité RED, capté le jour même.** Le refactor `-S` (`9d30939`) éditait
`regression/README.md` (+11 l) **sans rebuild** de `quality_report.json` (l'audit 4Big score le
contenu doc → champ `evidence` byte-count) → `check-artifacts` **ROUGE** (32 PASS · 1 FAIL). Rattrapé
sous ~17 min par `9fa464d` (rebuild → 1 ligne, verdict `PASS` 24/24 inchangé). Même classe
`rebuild-after-doc-edits` que l'incident du matin (`127ef6f`→`1321983`). Le rapport n'en consignait
qu'**un** ; ce 2ᵉ était absent → honnêteté du canal rétablie (cf. [[audit4big-rebuild-after-doc-edits]],
[[two-logging-channels]]).

**Fix.** Ajout d'une section « Mise à jour fin de journée · `184329` » au `daily_reports/2026-08-05.md` :
(1) tableau avant/après `-S` + reformulation de la ligne Régression ; (2) tableau du 2ᵉ incident ;
(3) veille lecture-seule depuis `f311a1b` (docstring helpers `db91a6b` CLEAN · gate constant-anchor
`1441693` · invariant INV-C `PYTHONUTF8` `1a44dfe` — **pas un nouveau job**, 33 inchangé) ;
(4) ré-attestation HEAD `1a44dfe`. Tables du matin **laissées intactes** (snapshots datés), réconciliées
explicitement par la nouvelle section (même convention que l'évolution 32→33 jobs).

**Ré-attestation (recompute `python3` indépendant, HEAD `1a44dfe`).** 4Big : **24 modules · tous == 100** ·
`coverage.ok=true` · `PASS`. Régression : **624 ran · 607 passed · 17 skipped · 0 fail / 0 err** · `PASS`.
Recette : **15/15 `in_repo`** · `ci_job` ×36 · `True`. Structure : `run_ci` **33 PASS** = 8 gates statiques
(`ls ci/*.sh` hors `lib.sh` = 8) + 25 suites.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate
ajouté/modifié** (#5), **0 chiffre saisi à la main** (#6 — 24/607/17/33/36 recomputés des artefacts +
`run_ci.sh`), **aucune commande VPS** (#8 — 100 % local, lecture-seule sur les artefacts). Seules
éditions : le rapport stakeholder + ce journal.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché).

## Session 181325 · FIX RÉEL défaut locale-runner : le gate est ROUGE en silence sous locale ASCII → **VERROUILLÉ UTF-8**

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS**, arbre propre, roadmap
`ROADMAP_8_WEEKS_OR_LESS.md` **intégralement livrée/gatée in-repo** : les 15 promesses de
l'`acceptance_matrix` sont **toutes `status=in_repo`**, chaque reste (BIM runtime, builds EAS,
dépôts ONAPI, déploiement prod) explicitement `out_of_scope` VPS (#8). Un fan-out Explore
docstring-vs-code / prose-vs-numérique sur les livrables → **0 dérive** (repo propre). J'ai donc
attaqué un **axe de robustesse absent de la mémoire** et, contrairement aux 3 sweeps déterministes
précédents (hash-seed / locale-TZ / forward-compat, tous **CLEAN → attestation**), celui-ci a
révélé un **défaut RÉEL présent**.

**La faille — la coercition PEP 538 masquait le vrai cas.** Le sweep locale-TZ (`154314`) a rejoué
les builds sous `LANG=C`, mais Python **coerce silencieusement `C` → `C.UTF-8`** (PEP 538) : sous
`LANG=C` seul, `getpreferredencoding()` = **UTF-8** → le mode d'échec ASCII n'a **jamais été
exercé**. En neutralisant la coercition (`PYTHONUTF8=0 PYTHONCOERCECLOCALE=0 LC_ALL=C`),
l'encodage par défaut redevient **`ANSI_X3.4-1968` (ASCII)** — la config d'un **runner minimal**.

**Constat — 20 des 21 générateurs `build` PLANTENT.** Réplique de la boucle `check_artifacts` sous
locale ASCII neutralisée : **20/21 builds crashent** (`UnicodeEncodeError`), les 25 suites
échouent aussi. Cause : chaque générateur clôt `cmd_build` par un `print("✅ Bundle … généré" /
accents)` vers **stdout**, dont l'encodage suit la locale. Les fichiers sont écrits sans souci
(`encoding='utf-8'` explicite partout : 37 writes annotés, 0 write nu), mais le **print cosmétique
final** lève `UnicodeEncodeError` **après** écriture → exit non-nul → `check-artifacts` voit
« build a échoué » **ROUGE**. Vert aujourd'hui (runner UTF-8), **rouge en silence** le jour où
l'image runner change = classe « le runner n'est pas ton poste », mais **défaut, pas attestation**.

**Correctif — single-source à la frontière d'invocation, 0 fichier de production.** `PYTHONUTF8=1`
(PEP 540) impose stdout/stdin/défaut-fichier en UTF-8 **quelle que soit la locale**. Posé **une
fois** en `env:` **au niveau workflow** de `ci.yml` → s'applique à **tous les jobs** (21 builds +
25 suites + gates statiques). Miroir local : `run_ci.sh` `export PYTHONUTF8=1`. **Aucun des 20
modules touché** (anti-churn #5 — la fragilité est un contrat d'environnement d'invocation, pas 20
bugs à patcher un par un ; le fix vit là où le runner est configuré, comme le gate `cd`-ait déjà
dans le module pour fixer le contrat de CWD).

**Verrou — l'absence du fix est INVISIBLE sur runner UTF-8** (retirer l'`env:` reste vert
aujourd'hui) : classe de « vert trompeur » exactement traquée par ce dépôt. J'ancre donc le fix dans
le gate **existant** `check_ci_integrity.sh` (**INV-C**, pas de nouveau fichier gate #5) : il lit
l'`env:` top-level de `ci.yml` et exige `PYTHONUTF8: "1"`. **Teeth mutation-vérifié** : retrait de
la ligne `env:` → INV-C **mord** (exit 1) ; restauré → vert. INV-A/INV-B intacts (l'`env:` est
avant `jobs:` → hors du parseur de jobs).

**Preuve end-to-end.** `run_ci.sh` sous `env -u PYTHONUTF8 PYTHONCOERCECLOCALE=0 LC_ALL=C` (config
qui était **totalement ROUGE** avant) = **33 PASS · 0 FAIL** grâce au `export` miroir. Build-loop
sous `PYTHONUTF8=1 + LC_ALL=C` = **21 artefacts byte-identiques** (0 dérive, le fix ne change aucun
octet de sortie). `run_ci.sh` normal = **33 PASS**.

**Portée / anti-churn.** 4 fichiers : `ci.yml` (env), `run_ci.sh` (miroir), `check_ci_integrity.sh`
(INV-C verrou), `ci/README.md` (§2 détail INV-C + « Deux → Trois invariants »). **0 fichier de
production**, **0 artefact rebuild** (aucun `out/` touché → pas de rebuild `quality_report`, cf.
[[audit4big-rebuild-after-doc-edits]]), **0 chiffre inventé** (#6 — 20/21 dérivés de la boucle),
**0 commande VPS** (#8). run_ci = **33 PASS · 0 FAIL · 0 SKIP**. Mémoire neuve
*utf8-locale-runner-fix* (sœur de [[locale-tz-determinism-sweep]] dont elle **corrige l'angle mort**).

---

## Session 164319 · FIX régression RED : `quality_report.json` périmé après édition de doc → **VERT**

**Constat au démarrage.** `./run_ci.sh` = **32 PASS · 1 FAIL** — `check-artifacts` RED sur
`qa/audit_4big/quality_report.json` (« DÉRIVE : le fichier commité diffère du build frais »).
Le tree HEAD `9d30939` (« Auto exec · session 161319 ») était commité **RED**. Cause racine :
cette session a grossi `05_deliverables_mvp/qa/regression/README.md` (+11 lignes, 4396→4948 octets)
mais **n'a pas rejoué** `audit_4big build`. Or l'audit 4Big **score le contenu des docs** et
consigne la taille de chaque README dans son champ `evidence` → drift mécanique, exactement la
classe [[audit4big-rebuild-after-doc-edits]] (« rebuild quality_report LAST, après toute édition de
prose »).

**Fix.** `python3 audit_4big_gen.py build -o out` → seul champ modifié :
`evidence: "README.md (4396 octets)"` → `"(4948 octets)"`. **Verdict inchangé PASS · 24/24 modules
≥ 95 (min 100)**, `coverage.ok=true` : la régression était purement l'echo d'une taille de fichier,
pas une perte de qualité. `MANIFEST.json` non touché (cohérent → `check_artifacts` revenu VERT).

**Recompute indépendant (aucun chiffre saisi · #6).** Diff commité↔build frais = **1 ligne** (le
byte-count). `run_ci.sh` post-fix = **33 PASS · 0 FAIL · 0 SKIP** (8 statiques + 25 suites).

**Portée / anti-churn.** **0 fichier de production édité** (seul l'artefact régénéré), **0 gate
ajouté** (#5 — la dérive était **déjà détectée** par `check_artifacts`, c'est la discipline
« rebuild LAST » qui a manqué, pas une gate), **0 chiffre saisi** (#6 — 4948 dérivé de la taille
réelle du README par le générateur), **aucune commande VPS** (#8). Classe connue en mémoire ;
attestation que le gate `check_artifacts` **mord sur le tree commité** (RED opposable, pas seulement
pré-commit) — jumelle de l'incident `144304`.

**Vérif.** `git diff --stat` = 1 fichier / 1 ligne · `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.

---

## Session 154314 · SWEEP neuf : indépendance LOCALE / TIMEZONE des artefacts `build` → **CLEAN**

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo (restes
S3/S5/S6 = refactor BIM / builds EAS / dépôts ONAPI / déploiement prod = **hors périmètre worker**,
#8). Un audit fan-out (Explore) docstring-vs-code + prose-vs-numérique sur les livrables → **CLEAN**,
0 drift présent. Le canal `daily_reports` est déjà courant (la ligne hash-seed `151312` a été ajoutée
par `a39c05a` lui-même). Choix d'un axe de **déterminisme absent de la mémoire et orthogonal** aux
deux précédents.

**Constat — la fragilité ciblée.** Le sweep hash-seed (`151312`) a couvert `PYTHONHASHSEED` mais
**PAS** l'environnement **locale/timezone** du runner. `check_artifacts`/`check_regression` prouvent
la reproductibilité byte-à-byte, mais **une seule fois**, sous **le** `LANG`/`LC_ALL`/`TZ` que le
runner Gitea leur donne. Si un générateur formatait un nombre via `locale` (décimale-virgule sous
`de_DE`/`fr_FR`), triait des clés via `locale.strcoll`, ou embarquait un timestamp `localtime`, sa
sortie varierait d'un runner à l'autre : **VERT sur le poste dev, ROUGE sur un runner sous locale/TZ
différente** — faux-vert *forward-looking* orthogonal au hash-seed (ordre de hash) et au `-W error`
(API dépréciée). Même famille « le runner n'est pas ton poste », cause différente.

**Méthode (lecture seule).** Réplique exacte de la boucle `check_artifacts` — pour chaque module
avec `out/`, le `.py` portant `add_parser("build")` est rejoué `build -o $tmp` puis `diff` vs commité
— mais sous **5 combos** `LANG`/`LC_ALL`/`TZ`/`LC_NUMERIC` : `C/UTC`, `en_US/America-New_York`,
`fr_FR/Europe-Paris`, `de_DE/Asia-Kolkata`, `POSIX/Pacific-Kiritimati`. **21 générateurs `build`
× 5 combos = 105 rejeux**. Tree resté propre (`git status --short` vide avant/après).

**Résultat — CLEAN + preuve honnête à deux volets.** **0 dérive / 0 échec sur les 105 rejeux.**
Le volet **TZ est authentiquement exercé** (5 fuseaux, `zoneinfo` présent). Le volet **locale a une
limite empirique** — seules `C`/`C.utf8`/`POSIX` sont installées ici, donc `fr_FR`/`de_DE`/`en_US`
retombent en C locale par glibc : le cas décimale-virgule n'est pas *empiriquement* prouvé. Il l'est
en revanche **par audit du code**, qui est la preuve définitive : (a) **0 générateur** n'appelle
`locale.setlocale`/`import locale` → la sérialisation `json`/`float` reste `'.'` (locale-indépendante
par design Python, la source de risque décimale-virgule **n'existe pas dans le code**) ; (b) les
**2 seuls** lecteurs d'horloge murale du corpus — `publiciste/lib/parser.py:428` et
`faisabilite/generator/faisabilite_gen.py:51` — n'ont **aucun `out/` commité** (leur timestamp
`generated_at` n'est jamais un artefact gaté) **et** forcent tous deux `datetime.now(timezone.utc)`
(doublement TZ-safe) ; (c) `regression_run.json` ne porte **aucun champ horloge**. Conclusion : le
runner CI peut tourner sous **n'importe quelle locale/TZ**, `check_artifacts`/`check_regression`
restent VERTS.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit** (aucun
`out/*.json` touché → pas de rebuild `quality_report`, cf. [[audit4big-rebuild-after-doc-edits]]),
**0 gate ajouté** (#5 — attester ≠ multiplier les gates ; propriété re-jouable via la commande, pas
une classe de dérive), **0 chiffre saisi à la main** (#6 — 21/105 dérivés de la boucle de découverte
`add_parser("build")`), **0 commande VPS** (#8 — 100 % local & lecture seule). Éditions : ce journal
+ 1 ligne dans la table « Veille d'intégrité » du canal stakeholder + 1 mémoire neuve
(*locale-tz-determinism-sweep*, sœur de [[hash-seed-determinism-sweep]] et
[[forward-compat-warnings-sweep]]).

**Vérif.** `./run_ci.sh` après → **33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché).

---

## Session 151312 · SWEEP neuf : déterminisme sous hash-seed aléatoire des gates de reproductibilité → **CLEAN**

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo (restes
S3/S5/S6 = refactor BIM / builds EAS / dépôts ONAPI / déploiement prod = **hors périmètre worker**,
#8). Sept sweeps déjà consignés ce jour (docstring-vs-code, forward-compat warnings, roadmap↔repo,
ré-attestation canal…). Choix d'un axe d'intégrité **absent de la mémoire et orthogonal** à tous
les précédents.

**Constat — la fragilité ciblée.** Les gates `check_artifacts` et `check_regression` prouvent que
chaque `out/*.json` versionné se régénère **byte-identique** depuis son générateur. MAIS ils ne
tournent **qu'une fois**, sous **le** `PYTHONHASHSEED` que le runner Gitea leur donne — et depuis
Python 3.3 ce seed est **randomisé par défaut**. Si **un seul** générateur sérialise un `set`/`dict`
en **ordre d'itération** (au lieu de trier), sa sortie varierait d'un seed à l'autre : le gate
serait alors **VERT aujourd'hui, ROUGE au hasard demain** sur le runner — un faux-vert
*forward-looking* qu'aucun rejeu à seed fixe n'attrape. C'est le pendant « déterminisme » du sweep
`-W error` (forward-compat interpréteur) : même famille, cause différente (ordre de hash vs API
dépréciée).

**Méthode (lecture seule · 0 édition de prod).** Rejeu de **tous** les générateurs `build` des
**21** répertoires `out/` vers un `mktemp -d`, sous **5 seeds** (`1 7 42 1000` + `random`), chaque
fichier produit `diff`-é contre son jumeau commité. Puis `regression_run.json` (artefact d'**exécution**,
produit par `run` — hors périmètre `check_artifacts` mais **dans** celui de `check_regression`) rejoué
séparément sous `1 7 42 999 random random`. Harnais auto-vérifié : **57 fichiers effectivement
comparés**, **1 seul skip** = `qa/regression/regression_run.json` (artefact `run`, exactement
l'exclusion documentée de `check_artifacts`) — donc zéro faux-vert par skip silencieux.

**Résultat — CLEAN.** **57 artefacts `build` byte-identiques sous les 5 seeds** + `regression_run.json`
**byte-identique sous 6 rejeux dont 2 `random`**. **Aucun** générateur ne dépend de l'ordre d'itération
d'un `set`/`dict` pour sa sérialisation → les deux gates de reproductibilité sont **robustes au
hash-seed**, ils ne peuvent pas flaper ROUGE sur le runner à cause de la randomisation. Le corpus
est prêt à un runner à seed non contrôlé sans régression aléatoire.

**Portée / anti-churn.** **0 fichier de production édité** (`git status --short` vide avant/après le
sweep), **0 artefact reconstruit** (les rebuilds vont en `/tmp`, jamais dans `out/` → pas de rebuild
`quality_report`, cf. [[audit4big-rebuild-after-doc-edits]]), **0 gate ajouté** (#5 — c'est une
**propriété re-jouable** via la commande, pas une *classe de dérive* : attester ≠ multiplier les
gates, cf. le sweep forward-compat), **0 chiffre saisi à la main** (#6 — 21/57 dérivés de
`find … -name out` et du décompte de comparaisons), **aucune commande VPS** (#8 — 100 % local &
lecture seule). Éditions : ce journal + une ligne dans la table *veille d'intégrité* du daily_report
+ une mémoire neuve. `./run_ci.sh` après → **33 PASS · 0 FAIL · 0 SKIP**.

---

## Session 114227 · AUDIT read-only roadmap↔repo (cross-refs numériques + liens) → **CLEAN** · clôture d'un **faux-positif « 7 dashboards vs 6 »**

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo, daily_report `2026-08-05.md`
déjà écrit ce jour (session 004112). Aucune tâche fonctionnelle in-repo → audit ciblé d'une classe
peu balayée : **cohérence des chiffres/liens de la roadmap vs l'état réel du dépôt** (table
« Gains d'accélération » + « Métriques succès MVP »), via un agent Explore + vérification manuelle.

**Un seul candidat remonté — vérifié NON-défaut (faux-positif d'arithmétique).**
La roadmap annonce « **7 dashboards uniformes** » (L39, DELIVERABLE Sprint 2 · et L83, métrique) alors
que la ligne L36 énumère « clone `/waf-home` sur **5** entités » (`/hrd-vente`, `/ac-construction`,
`/ecr-rh`, `/ploutos`, `/admin-9060`). L'agent a lu `1 base + 5 clones = 6` et signalé une
contradiction. **C'est faux** : le 7ᵉ dashboard est **`wag-home`**, page-home **déjà existante**
distincte de `waf-home`, attestée par `GAP_ANALYSIS_SPRINT1.md:91`
(« pages Frappe `www/` (**wag-home, waf-home**, pole/, projets/) »). Donc
**`wag-home` + `waf-home` + 5 clones entités = 7**. Le « 7 » est **sourcé et cohérent** entre la
roadmap (L39/L83) et la GAP (L93 « cloner `/waf-home` × 5 … → **7** dashboards uniformes ») ; aucune
occurrence littérale « 6 dashboards/entités » n'existe → **pas de contradiction interne**. WAG (parent
d'Helios RD, `CLAUDE.md` §Entités) a légitimement son propre dashboard. Ces dashboards sont par
ailleurs des **pages front VPS hors-repo** (aucun générateur/artefact `dashboard` in-repo — la
seule livraison frontend gatée est `portails/` = 5 Workspaces rôle, S4, classe distincte) : **aucune
source in-repo n'autorise à recompter**. Éditer `7→6` **inventerait** une correction fausse (#6) et
serait un churn (#5). **Décision : LEAVE / ne pas re-signaler** — famille connue du « 6-vs-7 sections »
financement déjà tranchée (`spec/liste réelle = autorité`, [[docstring-vs-code-drift]]).

**Reste de l'audit — CLEAN.** RBAC **50 rôles** recompté = 50 (`rbac/rbac_50_roles.json`) ; **5 portails**
= 5 Workspaces (`frontend/portails/README.md`) ; **Expo 51** actuel vs cible 54 conforme
([[mobile-runtime-actual-vs-rebuild-target]]) ; **0 lien markdown cassé** dans `03_agents/*/AGENT.md`
et `05_deliverables_mvp/*/README.md` (seuls non-résolus = chemins VPS `/opt/oto/…` + code-spans, par
design) ; aucune auto-contradiction numérique ailleurs.

**Portée honnête / anti-churn.** **0 fichier de production édité** (#5), **0 gate ajouté** (le « 7 »
est aspirationnel/hors-repo, non gatable — #8), **0 chiffre inventé** (#6), **0 commande VPS** (#8),
**aucun `git clean`**. Seules éditions : ce log + une mémoire de skip (`[[roadmap-7-dashboards-not-drift]]`).
`./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé). <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 111224 · SWEEP read-only 3 classes de dérive doc → **CLEAN** · + consignation d'un **NON-défaut vérifié** (le flag compat `-o` sur `validate`) pour clore une **récurrence de faux-positif** de l'audit docstring-vs-code

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo (les restes S3/S5/S6 = refactors
BIM/VPS **hors périmètre worker**, #8). Aucune tâche fonctionnelle in-repo → chasse à la dérive réelle
non-gatée, sur **trois classes** en parallèle (deux via Explore + vérif manuelle) :

1. **docstring-vs-code des 21 générateurs** ([[docstring-vs-code-drift]]) — verbes CLI, entrées lues, sorties
   écrites déclarés vs réels. **CLEAN** (déjà attesté ce matin par 104219/84df56a ; re-balayé).
2. **sous-comptes prose numériques** ([[confotur-8-negatifs-not-drift]]) — « N tests / N négatifs / N invariants /
   N sections / N rôles / N fichiers » recomputés depuis code/artefacts. **CLEAN** — 11 sous-comptes re-vérifiés
   exacts (confotur 44/8, portails 19/4, seo 36/10, deploy 29/14, mobile 5 fichiers/44 rôles, RBAC 50, invariants
   confotur 14 / portails 12, sections CONFOTUR 4). Aucune dérive (le seo 8→10 était déjà fixé fbe298f).
3. **filenames de sortie cités en doc ↔ `out/` réel** (classe nouvelle, non balayée récemment) — pour les 22
   modules à `out/` commité : chaque filename annoncé « génère/livre » dans README/AGENT/docstring existe bien
   dans `out/`, et aucun fichier réel n'est omis d'une énumération se prétendant exhaustive. **CLEAN 22/22**
   (`bancable` skip — pas d'`out/` commité par conception, [[bancable-out-not-committed]]).

**Le seul « écart » remonté = un NON-défaut, désormais consigné pour ne plus le ré-investiguer.**
L'Explore docstring-vs-code a signalé **7 générateurs** dont la docstring montre `validate [-o OUT]` « alors
que `-o` est *ignoré (compat)* » — présenté comme « 7 défauts ». **Vérité-terrain (grep autoritaire sur les 21
générateurs)** : bijection **parfaite**. Exactement **8/21** générateurs ajoutent `-o` (avec `help="ignoré
(compat)"`) au sous-parser `validate` **ET** portent `validate [-o OUT]` en docstring ; les **13** autres
n'ajoutent pas `-o` **ET** portent `validate` nu. Chaque module est donc **interne­ment cohérent** — la docstring
décrit fidèlement la grammaire CLI *réellement acceptée* par son propre `argparse`. **Zéro dérive docstring-vs-code.**

| validate accepte `-o` ? | docstring | générateurs |
|---|---|---|
| **+o** (compat, ignoré) | `validate [-o OUT]` | deploy_runbook · chat_otoia · portails · app_config · acceptance · rbac/fixtures · rbac/roleprofile · rbac/userperm (**8**) |
| **noO** | `validate` | commissions · dossier_vente · financement · workflow_vente · demo/scenarios · bancable · faisabilite · ecf_dgii · confotur · pie · audit_4big · audit_5d · regression (**13**) |

**Décision : LEAVE (anti-churn #5), pas de fix, pas de gate.** Ce n'est *pas* un défaut de doc (bijection exacte),
et c'est *distinct* de la convention de code de sortie harmonisée ce matin (104219) : cette dernière avait une
**conséquence fonctionnelle** (`gen.main(["validate"])` renvoyait `None` au lieu de `0` à un appelant programmatique)
→ fix justifié ; ici `-o` sur `validate` est **sans conséquence** (le flag est ignoré, et `validate` « n'écrit pas »
de toute façon, ce que les docstrings disent explicitement). Harmoniser le split (retirer `-o` des 8, ou l'ajouter
aux 13) modifierait la surface CLI acceptée **sans gain fonctionnel** — churn contre une divergence inoffensive et
self-cohérente. **Ce n'est pas non plus un item du registre** `OPEN_DECISIONS_REGISTER.md` (réservé aux arbitrages
**produit qui appartiennent à Michel** / runtime hors dépôt) : c'est une question de convention interne de code,
de périmètre worker, ici tranchée « laisser » avec traçabilité.

**Valeur durable de la session.** Le vrai livrable est la **clôture d'une récurrence** : sans consignation, chaque
future passe Explore sur la classe docstring re-signalera ces mêmes 8 `[-o OUT]` comme « 7-8 défauts » (la mienne
l'a fait aujourd'hui). Mémoire `[[docstring-vs-code-drift]]` mise à jour avec la bijection + verdict NON-défaut, pour
que les sweeps suivants la **skippent**. Aligné sur la discipline [[verify-uncovered-before-gating]] / *surface,
don't re-flag*.

**Portée honnête / anti-churn.** **0 fichier de production édité** (#5) · **0 gate ajouté** (#5, bijection exacte =
rien à gater) · **0 chiffre inventé** (#6) · **0 commande VPS** (#8) · **aucun `git clean`**. <!-- ci-allow : mention de l'interdit, jamais exécuté --> Touché : ce log + la
mémoire. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (arbre inchangé, aucune régression possible).

---

## Session 104219 · FIX de COHÉRENCE 4Big — `qa/acceptance/acceptance_gen.py` était le **SEUL outlier sur 23 générateurs** à rompre la convention universelle de code de sortie : `main()` typé `-> None` + appel nu `main()` (au lieu de `-> int` + `raise SystemExit(main())` chez les 22 autres) → aligné (`cmd_build`/`cmd_validate`/`main` → `int`, `return 0`, `raise SystemExit(main())`) · comportement inchangé · artefacts byte-identiques · 0 chiffre inventé · 0 gate ajouté

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée. Aucune tâche fonctionnelle in-repo
restante → chasse à la dérive réelle non-gatée. **Audit end-to-end de la classe *docstring-vs-code drift***
(récurrente/non-close · [[docstring-vs-code-drift]]) : sweep des docstrings de sortie des 23 générateurs +
des modules `*lib/` helper (que l'Explore scopé `*_gen.py` rate). **Tous vérifiés exacts** : `financement`
7 fichiers out ✓, `seo` 4 hand-off ✓, `confotur` 14 invariants (ledger `# N ·` numéroté 1..14) ✓,
`userperm` ✓ ; le parser `publiciste/lib/parser.py` mappe fidèlement ses 6 sources→§ (§1.1 localisation,
§3.2 typologies, §3.3 services) — aucun phantom résiduel (cf. l'ancien §5.3 déjà corrigé). Root README
`24/24 · 15 promesses (8+7)` re-dérivé des artefacts (`quality_report.coverage`, `acceptance/MANIFEST`) =
exacts. **Classe doc-drift saturée aujourd'hui.**

**Le seul écart concret trouvé (cohérence de code, pas dérive doc).** Un Explore sur la classe a signalé
`acceptance_gen.py` comme atypique. Vérité-terrain (`grep` `def main` + bloc `__main__` des 23) :
**22/23 générateurs** exposent `def main(...) -> int:` retournant `0`, `cmd_* -> int`, et
`raise SystemExit(main())` dans `__main__` — la CLI **propage un vrai code de sortie**. `acceptance_gen`
seul : `main(argv=None) -> None` + `cmd_build/cmd_validate -> None` + appel nu `main()`. Le chemin d'échec
restait correct (`_validate_or_die` → `sys.exit(1)` interne, L280), **donc pas un bug fonctionnel** — mais
un appelant programmatique (`rc = gen.main(["validate"])`, la **raison d'être** du paramètre `argv` que
tous les frères exposent pour la testabilité) recevait `None` au lieu de `0`. **Défaut de cohérence 4Big
réel** (1 outlier / 23), pas une invention.

**Fix minimal, comportement préservé.** 4 annotations `-> None`→`-> int`, deux `return 0` en fin de
`cmd_*`, `return args.func(args)`, `main()`→`raise SystemExit(main())` (7+/5− sur 1 fichier). Le chemin
d'échec `sys.exit(1)` est **inchangé** (succès=0, échec=1 comme avant). Les tests n'assertent que
« succès ne lève pas » (`test_validate_exits_zero` : `gen.main(["validate"])` sans vérifier le retour) →
inchangés au vert.

**Vérifications.** `python3 -m unittest` = **Ran 37 tests · OK** · `main(["validate"])` **renvoie 0**
(était `None`), `validate` rc=**0** · rebuild `build -o /tmp` → `acceptance_matrix.json` + `MANIFEST.json`
**byte-identiques** à `out/` (aucune dérive d'artefact · [[artifact-reproducibility-gate]], pas de rebuild
de consommateur nécessaire) · `./run_ci.sh` final : **33 PASS · 0 FAIL · 0 SKIP**.

**Bilan.** 1 fichier touché · **0 changement de comportement** (exit 0/1 identiques) · **0 artefact modifié**
· **0 gate ajouté** (#5 · la convention `-> int`/`SystemExit(main())` reste une norme de style non gatée,
défaut isolé unique — comme les typos d'origine, ne justifie pas un 34e check) · **0 chiffre inventé** (#6)
· **0 commande VPS** (#8) · **aucun `git clean`** <!-- ci-allow : mention de l'interdit, jamais exécuté -->.

## Session 094211 · FIX RÉEL — le README module **SEO** annonçait « 36 tests (dont **8** injections négatives) » alors que la suite en compte **10** `test_negative_*` (faux depuis le commit d'origine `0d3b242`, jamais gaté) → corrigé **8→10** ; toute la classe « sous-compte négatif en prose » (seo/devops/confotur) auditée · 0 gate ajouté · 0 chiffre inventé

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (Sprint 8 · QA). Aucune tâche
fonctionnelle in-repo restante → chasse à la **dérive documentaire réelle non-gatée** (le mode de valeur
honnête de cette phase). Recherche fan-out (Explore, read-only) + vérification manuelle : **une** dérive
présent-tense confirmée, isolée.

**Finding (calcul de la vérité-terrain, pas un soupçon · [[prose-facts-vs-numeric-drift]]).**
`05_deliverables_mvp/seo/README.md:67` écrit `# 36 tests (dont 8 injections négatives)`. Le **total 36**
est exact ET **gaté** (`ci/check_readme_claims.sh:941` recompute `# (\d+) tests` vs le nombre réel de
`def test_`). Le **sous-compte « 8 »** n'est capté par **aucun** groupe de capture (le pattern ne prend que
le total) → **non gaté**. Vérité-terrain : `grep -c "def test_negative_" tests/test_seo.py` = **10** (10
méthodes réparties dans les classes `Keywords`/`SchemaOrg`/`Hreflang`/`Manifest`, pas de classe dédiée).
`git show 0d3b242:…/test_seo.py | grep -c def test_negative_` = **10** dès le commit d'origine → le « 8 »
est **faux depuis la création** (2026-07-30), jamais un état vrai antérieur (donc pas *historical-repro*).

**Écarter le piège confotur ([[confotur-8-negatifs-not-drift]]).** Les deux modules-frères qui affichent
un sous-compte négatif en prose sont **exacts** car adossés à une **classe nommée** (structurellement
countable) : confotur « 8 négatifs » = classe `TestNegative` (**8** méthodes) ✓ · devops « 14 injections
négatives » = classe `TestNegativeInjections` (**14** méthodes) ✓. **SEO est le seul faux** : ses négatifs
ne forment **pas** de classe (préfixe `test_negative_` dispersé), et aucune lecture ne donne 8 → **10** est
le compte honnête. Fix minimal : `8 → 10`.

**Surfaces (balayage twin-fix · [[prose-facts-vs-numeric-drift]]).** Le « 8 » apparaît 3× : (a)
`seo/README.md:67` = **présent-tense, doc canonique vivante → FIX** ; (b)
`daily_reports/2026-07-30-session18.md:48` et (c) `05_activity_log/2026-07-30.md:920` = **snapshots datés
du 2026-07-30 → KEEP** (convention : rapports/logs horodatés immuables, ne pas réécrire l'histoire,
[[two-logging-channels]]).

**Effet de bord attendu et rebuild ([[audit4big-rebuild-after-doc-edits.md]]).** `audit_4big` **score le
contenu README** ; l'édition a fait diverger l'artefact byte-gaté `qa/audit_4big/out/quality_report.json`
(`check-artifacts` RED). Rebuild `audit_4big_gen.py build` **en dernier** → seule ligne changée :
`"evidence": "README.md (3900 → 3901 octets)"` (« 8 »→« 10 » = +1 octet) · verdict **PASS · 24/24 ≥95
(min 100)** inchangé.

**Décision de NON-gatage (#5 · [[verify-uncovered-before-gating]]).** Erreur d'**origine isolée** (une
faute de frappe unique), **pas** une classe de dérive récurrente : les 2 frères sont exacts et le total est
déjà gaté. Gater le sous-compte seul exigerait, par cohérence, de gater aussi confotur/devops (classe
countable, changement rare) — 3 checks pour un événement à très basse fréquence. Conforme au précédent
[[confotur-8-negatifs-not-drift]] (« ne pas fixer/gater » ces sous-comptes). **Classe désormais auditée
end-to-end** (consigné pour un futur passage : les 3 sous-comptes négatifs sont vérifiés exacts au
2026-08-05).

**Bilan.** 2 fichiers touchés (`seo/README.md` prose + `quality_report.json` régénéré) · **0 édition
d'autorité** (le test file, source de vérité, n'est pas modifié) · **0 gate ajouté** (#5) · **0 chiffre
inventé** (#6 · 10 dérivé de `grep -c`) · **0 commande VPS** (#8) · **aucun `git clean`** <!-- ci-allow : mention de l'interdit, jamais exécuté -->
· `./run_ci.sh` final : **33 PASS · 0 FAIL · 0 SKIP**.

## Session 091204 · FIX de COUVERTURE doc — la fiche **Mobile** omettait son propre **livrable in-repo gaté** (`mobile/app_config`) : elle n'ancrait le rôle que sur RBAC + acceptance, jamais sur le générateur de config app (22 tests · job CI · CLI) → sous-section dédiée + ligne de table **auto-gatée** (3 gates) + 3 énumérations « ancrage in-repo » réconciliées · 0 édition d'autorité · 0 gate ajouté · 0 chiffre inventé

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap intégralement livrée/gatée (Sprint 8 · launch-readiness). Un Explore read-only sur la
classe *docstring-vs-code drift* (récurrente, cf. [[docstring-vs-code-drift]]) n'a **rien remonté de
concret** (le « 14 invariants » de `chat_otoia` est correct : les invariants 8 et 9 sont assertés à la
fois globalement ET par-mount → 14 numéros distincts, pas une dérive). Classe saturée. Pivot vers la
**complétude des fiches** — la classe qui a produit le FIX réel de la session 071201 (CRM omettait son
4e module `financement_bancaire`) : *completeness ≠ accuracy* ([[fiche-accuracy-audit-closed]]).

**Le finding — diff `05_deliverables_mvp/*/` vs les 13 fiches.** Un `grep -rl <module> 03_agents/*/AGENT.md`
sur les 22 modules livrés a remonté **un seul orphelin** : `mobile/app_config` → référencé par **AUCUNE**
fiche (les 21 autres sont couverts ≥1×). Or `05_deliverables_mvp/mobile/app_config/` est un module
générateur **réel, commité et gaté** : 5 fichiers `out/*.json` (`app_config.json`/`eas_build.json`/
`role_navigation.json`/`store_listing.json`/`MANIFEST.json`), suite `mobile-app-config-tests` **dans
`gate.needs`**, CLI `build|validate`, 22 méthodes de test (`regression_plan.suites[mobile/app_config].
test_methods`). C'est le **livrable in-repo propre de l'agent Mobile** (docstring : « Livrable Mobile de
la roadmap Sprint 5 »). La fiche `03_agents/mobile/AGENT.md` ne le mentionnait **nulle part** — elle
énumérait ses ancrages in-repo comme *uniquement* le **rôle RBAC** + la **row QA acceptance S5**
(« ancré dans **deux** artefacts in-repo vérifiables »), en omettant son générateur le plus direct.
Ce n'était pas une inexactitude (les 2 ancres RBAC/QA sont justes) mais une **incomplétude** : le
livrable in-repo le plus concret de l'agent était absent de sa propre fiche.

**Pas de piège des deux cadres de sprint (contrairement à 071201/081202).** `mobile/app_config` est
Sprint **5** dans les DEUX cadres : `acceptance_spec` le classe en `evidence_modules` de **S5**
(`roadmap_line 58`) et la roadmap `## SPRINT 5` porte la ligne **Mobile L56** « Rebuild Expo 54 +
submit App Store #32 + Play Store ». J'ai cité **L56** (ligne Mobile, celle qu'ancre la docstring
`l.56-57`). Aucun conflit à trancher ([[agent-internal-vs-roadmap-sprint-frames]]).

**La correction (mirroir exact du pattern CRM 071201).** (a) Nouvelle sous-section « Générateur de
config app **in-repo** · `mobile/app_config` » + **1 ligne de table** identique en structure à celle du
CRM (`Module | Sprint | Rôle | Entrée CLI | Job CI | Tests`). (b) **3 réconciliations minimales** des
énumérations « ancrage in-repo » qui, sans elles, contrediraient la nouvelle section : le « livrable
in-repo est [refonte VPS] et [rôle RBAC] » (ajout du générateur), « ancré dans **deux** artefacts »
(→ « des artefacts … Outre son générateur … **deux cross-références** RBAC/QA »), et la note d'honnêteté
« l'ancrage réel est RBAC + QA S5 » (ajout du générateur). Toutes sourcées, aucune n'invente.

**Auto-gatage confirmé — le finding devient opposable au merge (3 gates indépendants de
`check_readme_claims.sh`, résolution par cible de lien `mobile/app_config ∈ plan.suites`).** `run_ci`
final montre les 3 dents mordre sur la nouvelle ligne (`AGENT.md:65`) : (a) cellule Tests `22 == source
(22)`, (b) job CI `mobile-app-config-tests == ci.yml`, (c) verbes CLI `build|validate == subparsers`.
Une future dérive du compte/job/CLI **mordra** désormais — comme pour la ligne CRM financement.

**Discipline.** Le `build` du module n'est **pas** régénéré (je n'ai touché aucun `out/`) → `git status`
ne montre **que** `03_agents/mobile/AGENT.md`. **0 gate ajouté** (surface déjà couverte par row_re/job/CLI
· #5) · **0 édition d'autorité** (roadmap, spec, directive, module intouchés) · **0 chiffre inventé**
(#6 : 22 lu dans `regression_plan`, 5 fichiers lus dans `out/` + docstring, Sprint 5/L56 lu dans
`acceptance_spec` + roadmap) · aucune commande VPS (#8) · aucun `git clean`. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**Bilan.** 1 fiche agent complétée (sous-section + 1 ligne auto-gatée + 3 réconciliations) · **CI 33
PASS · 0 FAIL · 0 SKIP** · le dernier module livré orphelin de fiche est désormais documenté et opposable.

## Session 081202 · FIX RÉEL — le module `crm/financement_bancaire` s'auto-étiquetait « **Sprint 5** » dans une **citation roadmap-globale** (`roadmap L52`) alors que L52 ∈ **Sprint 4** : label corrigé S5→S4 sur ses 2 seules surfaces (README + docstring gen) · directive Michel INTACTE · 0 gate · 0 chiffre inventé

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap intégralement livrée/gatée (Sprint 8 · launch-readiness). Tâche retenue : le **SIGNAL surfacé non
tranché** par la session 071201 — le README du module `crm/financement_bancaire` écrivait « **Sprint 5** ·
roadmap L52 » alors que L52 est une ligne **Sprint 4**. Investigué et **résolu comme drift réel** (pas un
cadre agent-interne).

**Le finding — un vrai conflit de cadres, tranché par la structure roadmap + l'autorité byte-gatée.**
La ligne 3 du README dit « **Sprint 5 · CRM natif ERPNext · roadmap L52 (workflow vente end-to-end · volet
financement)** ». C'est une **citation roadmap-GLOBALE** (elle cite `roadmap L52` + reprend le texte du
deliverable S4 « workflow vente end-to-end »), **pas** un cadre agent-interne de build-phase
([[agent-internal-vs-roadmap-sprint-frames]]). Or :
- Le **fichier roadmap** lui-même : `## SPRINT 4` = L48, deliverable L52 « 5 portails RBAC live · workflow
  vente end-to-end » ; `## SPRINT 5` = L54 « ONAPI + Compliance + Mobile » (rien à voir avec le financement).
  → « Sprint 5 · roadmap L52 (workflow vente end-to-end) » est **auto-contradictoire**.
- L'**autorité byte-gatée** `qa/acceptance/acceptance_spec.json` (partition INV5) classe
  `crm/financement_bancaire` sous **S4 · roadmap_line 52** (`evidence_modules` de S4).
- Les **3 modules frères** du même bucket S4 s'étiquettent tous « **Sprint 4** » (workflow_vente : « Réalise
  le deliverable roadmap Sprint 4 » · dossier_vente : « Sprint 4 » · commissions : « Sprint 4 · roadmap
  ligne 51 »). `financement_bancaire` était le **seul outlier**.
- La **fiche CRM** `03_agents/crm/AGENT.md` liste déjà correctement ce module en « **4 (roadmap L52)** »
  (session 071201, sourcé S4). Le module contredisait donc sa propre fiche.

**Provenance de l'erreur (surfacée, non éditée).** La **DIRECTIVE de Michel**
`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md:140` cadre le travail en « `[Sprint 5 · Financement]` » —
c'est **son** cadre d'entrée (snapshot daté, autorité pour son propre contenu · [[directive-vs-implementation]]).
L'auteur du module a transcrit ce « Sprint 5 » directive **dans un slot de citation roadmap-globale**, où il
devient faux (L52 ∈ S4). La directive est **laissée INTACTE** (input immuable de Michel, jamais éditée · comme
`daily_reports`).

**La correction (2 surfaces, balayées en un passage · [[prose-facts-vs-numeric-drift]]).** Grep exhaustif :
exactement **2** surfaces manuscrites portaient le mauvais label — `README.md:3` (prose) et
`financement_bancaire_gen.py:4` (docstring) ; **aucun** spec/artefact/`out/*.json`/gate ne porte de champ
sprint (vérifié). S5→S4 sur les deux. Le `build` du module ne régénère **aucun** artefact modifié (docstring
+ README non émis dans `out/`) → `check_artifacts` reste vert, `git status` ne montre que mes 2 fichiers source.

**Pas de gate ajouté (#5).** Le label de sprint des modules est un **piège à deux cadres légitimes**
(agent-interne vs roadmap-global) : un gate aveugle qui re-dériverait chaque label depuis `acceptance_spec`
produirait des faux positifs sur les frames agent-internes légitimes. Je **corrige la prose** (drift réel,
citation roadmap-globale fausse) sans instrumenter la surface. 0 chiffre inventé (#6) · 0 commande VPS (#8).

**Bilan.** 2 surfaces source alignées S5→S4 · directive Michel INTACTE · 0 édition d'autorité (l'autorité
`acceptance_spec` disait déjà S4) · 0 gate · 0 invention · **CI 33 PASS**. SIGNAL 071201 → **clos**.

## Session 071201 · FIX de COUVERTURE doc — la fiche CRM omettait son **4e module livré** (`crm/financement_bancaire`) : ajout d'une sous-section dédiée + ligne de table **auto-gatée** (35 tests · job CI · verbes CLI) · 0 édition d'autorité

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (24/24 modules 4Big à 100/100, matrice
d'acceptance 15/15 `in_repo`) — **Sprint 8 · launch-readiness**. Les décisions ouvertes restantes sont
des **arbitrages produit de Michel** ([[open-decisions-register]], ne pas trancher · #5). Tâche retenue :
le **fallback explicite** de la mission (« améliorer la doc d'un AGENT.md ») appliqué à un **écart réel**,
pas cosmétique.

**Le finding.** La fiche `03_agents/crm/AGENT.md` se présente comme le recensement des « **Livrables CRM
réellement produits** » mais n'en documentait que **3** (trio pipeline `workflow_vente`/`dossier_vente`/
`commissions`). Or `05_deliverables_mvp/crm/` héberge un **4e module gaté** — `financement_bancaire`
(parcours hypothécaire RD · gate check 4 conditions · 35 tests · job `crm-financement-bancaire-tests`) —
**absent de TOUTES les 13 fiches agents** (grep confirmé), y compris de sa fiche propriétaire naturelle.
Ce n'était pas une *inexactitude* (les 3 lignes existantes sont justes, cf. [[fiche-accuracy-audit-closed]])
mais une **incomplétude** : un livrable réel sans surface de doc côté agent.

**Pourquoi il était hors du trio (et le reste).** Le trio partage **une source unique**
(`workflow_vente_spec.json` + `rbac_50_roles.json`) — c'est la propriété « cross-cohérents » que décrit la
prose du trio, et que le gate `check_readme_claims.sh:725` encode en figeant « Total CRM : 81 tests » aux
**3** suites du pipeline. `financement_bancaire` **ne partage pas** cette source (piloté par
`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`), donc l'ajouter comme 4e ligne du trio aurait été
faux sémantiquement **et** cassé le gate (regex `Total CRM : **N tests**`). D'où une **sous-section
séparée**, le trio et son agrégat gaté laissés **intacts**.

**Sourcing (zéro invention · #6).** Sprint **4 · roadmap L52** lu dans l'autorité roadmap-globale
`qa/acceptance/acceptance_spec.json:71-84` (byte-gaté ; `evidence_modules` inclut `crm/financement_bancaire`
sous S4) — **pas** le « Sprint 5 » du README module, qui est le **cadre agent-interne**
([[agent-internal-vs-roadmap-sprint-frames]]). Nombre de tests **35** lu dans `regression_plan.json`
(`test_methods`, source faisant autorité). CLI `financement_bancaire_gen.py build|validate` vérifié depuis
les subparsers.

**Auto-gatage confirmé — le finding devient opposable au merge.** La nouvelle ligne de table est captée
par **trois** gates indépendants de `check_readme_claims.sh` (résolution par **cible de lien**,
`crm/financement_bancaire ∈ auth`) : (a) cellule Tests `35 == source (35)`, (b) job CI
`crm-financement-bancaire-tests == ci.yml`, (c) verbes CLI `build|validate == subparsers`. Une future
dérive du compte/job/CLI **mordra** désormais. `./run_ci.sh` final : **33 PASS**.

**SIGNAL (surfacé, non tranché · SURFACE don't rewrite).** Le README du module écrit « **Sprint 5** ·
roadmap L52 » — pairing interne étrange (L52 est une ligne **Sprint 4**) : soit le cadre agent-interne,
soit une micro-dérive du label roadmap-global vs l'autorité `acceptance_spec` (S4). **Non édité** (le
module README n'est pas ma cible ; le piège des deux cadres de sprint exige une édition spec+regen
coordonnée, cf. [[agent-internal-vs-roadmap-sprint-frames]]) — consigné ici pour un passage ultérieur.

**Bilan.** 1 fiche agent enrichie (sous-section + 1 ligne auto-gatée) · **0 édition d'autorité** · 0 gate
ajouté (surface déjà couverte par `row_re`/job/CLI · #5) · 0 chiffre inventé · CI **33 PASS**.

## Session 051154 · Attestation des DENTS des **suites de MODULE** — BATCH 4 (FINAL) : les **7 dernières** suites → **25/25** prouvées · **7 BITES · 0 TOOTHLESS** · arbre byte-pristine · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée — nous sommes au **Sprint 8 · QA :
regression tests exhaustifs**. Continuation et **clôture** de l'axe des DENTS des suites de MODULE
([[module-suite-teeth-mutation]]) : les batches 034144/041149/044153 ont prouvé **18/25**, laissant les
**7 dernières** non-couvertes (rbac-aggregate, portails, audit-4big, demo-scenario, regression,
deploy-runbook, acceptance) — **non-couvertes ≠ édentées, juste non-prouvées**. Ce batch les couvre
toutes → **25/25**. Axe distinct des 8 gates STATIQUES (8/8, 031134) : une suite qui rechargerait
`out/*.json` contre elle-même serait édentée = faux vert Sprint 8 comme un gate édenté.

**Méthode.** Reconnaissance (Explore, read-only) pour cartographier, par module, **une ligne de LOGIQUE
de générateur** reconstruite en mémoire par une assertion. **Vérification critique avant de croire
l'agent** ([[module-suite-teeth-mutation]] leçon (b)) : deux mutations proposées étaient **INERTES**,
pas mordantes — `and→or` sur `deploylib/builder.py:61` (`not [] and not []` = True → `or` = True aussi,
sortie inchangée) et idem `acclib/builder.py:138`. Remplacées par des mutations qui **font basculer** la
sortie observée : `deploy` → `gated_set - mapped_set` **→ `|`** (union : `missing` non-vide, bijective
False, asserté par `test_coverage_bijective`) ; `acceptance` → `"exact": want == have` **→ `!=`**
(chaque `p["exact"]` bascule False, asserté par `test_partition_exact_all_sprints`). Unicité de chaque
cible re-vérifiée par `grep -Fc == 1` **avant** mutation. Harnais `/tmp` read-only
(`/tmp/suite_teeth_20260805_batch4.sh`) : **baseline verte** sanity → backup `cp` → `str.replace` (assert
1 occurrence) → run de **la seule suite du module** (working-directory dérivé de `ci.yml`) → assert
`exit≠0` → restauration `cp` + `cmp` byte-exact ; `trap EXIT` restaure tout ; jamais `git
clean`/`checkout` ([[ci-gate-verification-method]]). <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**6 BITES — mutations de LOGIQUE de générateur, sortie re-assertée en mémoire :**
- **portails** · `wslib/builder.py:114` `roles_couverts = sum(m["nb_roles"] …)` **`+ 1`** → `test_roles_restriction` recompte le total vs `manifest.counts.roles_couverts`.
- **audit-4big** · `q4lib/builder.py:30` `"fail": len(below)` **`+ 1`** → `test_real_build_passes_gate` exige `totals.fail == 0`.
- **demo-scenario** · `scenlib/builder.py:44` `duree = sum(b["duree_min"] …)` **`+ 1`** → `test_duree_est_somme_des_beats` recompte `sc["duree_min"]` vs somme des beats.
- **regression** · `reglib/builder.py:44` `"test_methods": sum(s["test_methods"] …)` **`- 1`** → `test_totals_match_suites` recompute la somme vs `totals.test_methods`.
- **deploy-runbook** · `deploylib/builder.py:59` `gated_set - mapped_set` **`→ |`** → `test_coverage_bijective` exige `bijective` + `missing==[]`.
- **acceptance** · `acclib/builder.py:136` `"exact": want == have` **`→ !=`** → `test_partition_exact_all_sprints` exige chaque `p["exact"]` + `partition_ok`.

**1 BITE — sous-classe distincte (validation-de-CONTRAT, pas générateur).** `rbac-aggregate`
(`rbac/tests/test_rbac.py`) n'a **pas** de builder/`out/` : il charge directement le **contrat**
`rbac_50_roles.json` et y asserte des invariants absolus (schéma, ==50, unicité id/nom, couverture des 5
portails, invariant #6 `set_user_permissions`). Structurellement **non-echo** (il ne compare pas un
artefact à lui-même — il contraint des constantes dures). Dent prouvée par une dérive du **contrat**
`"cible_rbac_roles": 50 → 49`, assertée par `test_exactement_50_roles` → **BITE**. Honnêteté de portée :
axe « contrat » ≠ axe « logique de générateur », signalé comme tel plutôt que forcé dans le même moule.

**Verdict : 7 BITES · 0 TOOTHLESS → clôture 25/25 des suites de MODULE mordantes.** Combiné aux 8 gates
STATIQUES (8/8) et à la reproductibilité des artefacts, le Sprint 8 « regression tests exhaustifs » est
attesté sur ses **deux axes de DENTS**.

**Vérifications.** Arbre **byte-pristine** après harnais (`git status --porcelain` vide, `git diff --stat`
vide). `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché — restauration byte-exact).
**0 chiffre inventé (#6)** · **0 gate ajouté** (prouve les tests existants, n'en crée aucun — #5) · **0
édition de production** · **aucune commande VPS (#8)** · **aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 044153 · Attestation des DENTS des **suites de MODULE** — BATCH 3 : mutation-test *generator-logic* sur 6 nouvelles suites (→ **18/25** prouvées) · **6 BITES · 0 TOOTHLESS** · arbre byte-pristine · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée. Continuation directe de
l'axe des DENTS des suites de MODULE ([[module-suite-teeth-mutation]]) : les batches 034144 (6) et
041149 (6) ont prouvé **12/25** suites, laissant **13 non-couvertes (≠ édentées, juste non-prouvées)**.
Ce batch 3 en couvre 6 de plus → **18/25**. Axe distinct des 8 gates STATIQUES (8/8, 031134) : une
suite qui rechargerait `out/*.json` contre elle-même serait édentée = faux vert Sprint 8.

**Méthode.** Reconnaissance (Explore, read-only) pour cartographier, par module, **une ligne de LOGIQUE
de générateur** (calcul/règle, jamais un octet d'artefact) qu'une assertion unittest reconstruit **en
mémoire** depuis le contrat. Unicité de chaque cible re-vérifiée par `grep -Fc == 1` **avant** mutation.
Harnais `/tmp` read-only (`/tmp/suite_teeth_20260805_3.sh`) : backup `cp -p` → remplacement d'octets
littéral (`str.replace`, assert 1 occurrence) → run de **la seule suite du module** → assert `exit≠0` →
restauration `cp` + `cmp` byte-exact ; `trap EXIT` restaure tout ; jamais `git clean`/`checkout`
([[ci-gate-verification-method]]). <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**6 BITES (mutations de LOGIQUE réelles) :**
- `rbac/fixtures_gen` — `fixturelib/builder.py:43` `if_owner = scope == SCOPE_IF_OWNER` `== → !=` →
  inverse tous les `if_owner`, `test_if_owner_reflete_scope_own` (attend `1 if scope=="own" else 0`) **mord**.
- `rbac/userperm_gen` — `permlib/builder.py:43` `by_mechanism[mechanism] += 1` `→ += 2` → total 50→100,
  `test_manifeste_fidele` (`assertEqual(sum(by_mechanism.values()), 50)`) **mord**.
- `rbac/apply_plan` — `applylib/aggregator.py:174` chaîne `roles == cible == up_entries == rp_covered`,
  2ᵉ `== → !=` → `couverture_bijective` bascule False, `test_consistency_bijective` (`assertTrue`) **mord**.
- `mobile/app_config` — `mobilelib/builder.py:105` `roles = sorted(roles_map.get(key, []))` `+reverse=True` →
  ordre inversé, `test_roles_allowed_egal_surface_rbac` (`==sorted(roles_map[portail])`) **mord**.
- `qa/audit_5d` — `qalib/builder.py:60` `"fail": all_statuts.count(_FAIL)` `_FAIL → _PASS` → totals
  fail 0→13, `test_verdict_pass_with_open_items` (`totals == {...,"fail":0,...}`) **mord**.
- `frontend/chat_otoia` — `chatlib/builder.py:114` `"roles_couverts": sum(m["nb_roles"] …)` →
  `sum(1 …)` → 44→~5, `test_44_roles_couverts` (`total(44) == roles_couverts`) **mord**.

**LEÇON de méthode réappliquée — désambiguïser TOOTHLESS-test vs mutation INERTE.** L'Explore avait
d'abord proposé `mobile mobilelib/builder.py:192 "roles_couverts": sum → len` : **édenté pour la suite
de MODULE** car `roles_couverts` (agrégat manifest) n'est **jamais** ré-asserté en mémoire par la suite
mobile (seuls `n_roles`/`roles_allowed` par-onglet le sont). Observation honnête, **pas un défaut** — on
a muté à la place une ligne **réellement contrainte** (`:105` le tri). Idem `fixtures :68 doctypes_uniques`
(non référencé par la suite) → basculé sur `:43 if_owner`. → Règle confirmée : **vérifier que la ligne
mutée est bien celle que le test reconstruit avant de croire un verdict.**

**Portée honnête.** 18/25 échantillonnées pour la diversité (inversion booléenne, incrément de compteur,
chaîne d'égalité, tri-déterminisme, count de statut, agrégat de rôles). **7 restantes non-couvertes ≠
édentées** (rbac agrégé, portails, audit-4big, demo-scenario, regression, deploy-runbook, acceptance) —
pas de plafond silencieux.

**Vérifications.** Après restauration : `git status --porcelain` **vide**, arbre **byte-exact pristine**
(6/6 fichiers `cmp` OK). `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Harnais `/tmp` supprimé.
**0 gate ajouté (#5 — prouve les tests existants, n'en ajoute aucun) · 0 chiffre inventé (#6) · 0 édition
de production · 0 commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 041149 · Attestation des DENTS des **suites de MODULE** — BATCH 2 : mutation-test *generator-logic* sur 6 nouvelles suites (→ **12/25** prouvées) · **6 BITES · 0 TOOTHLESS** · arbre byte-pristine · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre. Continuation honnête et directe de l'axe des DENTS des suites de MODULE ([[module-suite-teeth-mutation]]) :
la session 034144 avait prouvé **6/25** suites, laissant **19 non-couvertes (≠ édentées, juste non-prouvées)**.
Ce batch 2 en couvre 6 de plus → **12/25**. Axe distinct des DENTS des 8 gates STATIQUES (8/8, 031134) : une
suite qui rechargerait `out/*.json` contre elle-même serait édentée = faux vert Sprint 8 comme un gate édenté.

**Méthode.** Reconnaissance (Explore, read-only) pour cartographier, par module, **une ligne de LOGIQUE de
générateur** (calcul/règle, jamais un octet d'artefact) qu'une assertion unittest reconstruit **en mémoire**
depuis le contrat. Unicité de chaque cible vérifiée par `grep -Fc` **avant** mutation. Harnais `/tmp` read-only
(`/tmp/suite_teeth_20260805_2.sh`) : backup `cp` → mutation de la logique → exécution de **la seule suite du
module** → assertion `exit≠0` → restauration `cp` + `cmp` byte-exact ; `trap EXIT` restaure tout ; jamais
`git clean`/`checkout` ([[ci-gate-verification-method]]). <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**6 BITES (mutations de LOGIQUE réelles) :**
- `faisabilite/generator` — `scorer._score_completude` `20 * filled → 20 / filled` (sed adressé L57 car la
  formule récurre L81) → le score `4big` tombe < 95, `test_complete_brief_scores_publiable` **mord** (exit 1).
- `faisabilite/bancable` — `finance.derived` point d'équilibre `math.ceil(pe_brut) → math.floor` → 21→20,
  `test_derived_arithmetic_traceable` **mord**.
- `crm/workflow_vente` — `builder` `trans_rows.sort(...)` `+reverse=True` → casse le déterminisme trié,
  `test_transitions_sorted_stable` **mord**.
- `crm/dossier_vente` — `is_submittable` seuil `>= "1"` (voir ci-dessous) **mord**.
- `crm/financement_bancaire` — `gate.apport_requis_usd` `/ 100.0 → * 100.0` → apport ×10 000,
  `test_apport_requis_*` **mord**.
- `publiciste` — `_fmt_usd` suppression du `.replace(",", " ")` (séparateur de milliers = espace) →
  "USD 150,000" ≠ "USD 150 000" attendu (test L201) **mord**.

**LEÇON de méthode — un verdict « TOOTHLESS » doit être désambiguïsé : (a) test réellement édenté vs
(b) mutation comportementalement INERTE pour la donnée du contrat.** Deux mutations de ce batch ont d'abord
lu TOOTHLESS mais étaient (b), **PAS des défauts** :
- `dossier_vente` `>= "1" → > "1"` est **inerte** car le max des `doc_status` réels = **"2"** (les deux
  branches donnent 1). Une mutation qui **fait basculer** le résultat (`>= "3"` → `"2">="3"` False → 0)
  **mord** bien : la suite contraint la règle `is_submittable`.
- `publiciste` `min(prices) → max` est **inerte** car le retour de `_prix_depuis` n'alimente QUE la gate
  `is not None` (L116) ; sa valeur min-vs-max n'est **jamais** affichée (chaque ligne de typologie rend son
  propre prix, L57). Observation honnête, **pas un défaut à corriger** (#5/#6) : la sémantique du minimum
  n'a aucun effet observable en aval → on mute plutôt une logique **observée** (`_fmt_usd`).
  → Règle : **confirmer que la mutation change la SORTIE pour la fixture avant de croire un TOOTHLESS.**

**Portée honnête.** 12/25 échantillonnées pour la diversité (arithmétique, seuil booléen, tri-déterminisme,
zfill/format, dédup, séparateur) ; **13 restantes non-couvertes ≠ édentées** (rbac-agrégat/fixtures/userperm/
applyplan, portails, audit-5d, chat-otoia, audit-4big, demo-scenario, regression, deploy-runbook, acceptance,
mobile-app-config) — pas de plafond silencieux.

**Vérifications.** Après restauration : `git status` **propre**, arbre **byte-exact pristine** (6/6 fichiers
`cmp` OK). `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier gaté modifié en net).
**0 gate ajouté (#5 — prouve les tests existants, n'en ajoute aucun) · 0 chiffre inventé (#6) · 0 édition de
production · 0 commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 034144 · Attestation des DENTS des **suites de test de MODULE** — mutation-test *generator-logic* (6 suites échantillonnées, chacune doit sortir NONZERO sur une dérive de LOGIQUE de son générateur, pas d'écho d'artefact) → **6 BITES · 0 TOOTHLESS** · arbre byte-pristine · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée. Vérifications
d'entrée : les 3 chiffres launch-readiness rechargent à l'identique depuis les artefacts commités
(recompute `python3` indépendant, aucune valeur saisie à la main) — `quality_report` **24 modules ·
tous == 100 · coverage.ok** · `regression_run` **suites 24 · ran 624 · passed 624 · 0/0/0** ·
`acceptance_matrix` **15/15 `in_repo` · verdict True · 36 `ci_job`** · `run_ci --list` **33 = 8 statiques + 25 suites**.
Un balayage read-only fan-out (README de module + docstrings des `*_gen.py`/`*lib` + DIRECTIVE↔module)
n'a **remonté aucune dérive prose** (surfaces contenu saturées, #5). **Axe distinct et jamais
couvert en suite** : hier (`031134`) a prouvé que les **8 gates STATIQUES** mordent (8/8) ; personne
n'a jamais prouvé que les **25 suites unittest de MODULE** mordent sur une dérive de **LOGIQUE de
générateur**. Une suite qui ne ferait que recharger `out/*.json` et l'asserter contre lui-même
serait **édentée** (faux vert Sprint 8 « regression tests exhaustifs ») exactement comme un gate
édenté. Constat encourageant préalable : les suites **reconstruisent en mémoire** depuis le contrat
(`builder.build_bundle(contract)`), donc elles *devraient* contraindre la logique — restait à le **prouver**.

**Méthode ([[verify-uncovered-before-gating]] appliquée à l'inverse — prouver que la protection MORD).**
Harnais unique hors dépôt (`/tmp`, jamais commité), par suite : `backup cp → mutation ciblée de la
LOGIQUE du générateur → run de la SEULE suite du module → assert exit≠0 → restore byte-exact cp →
cmp`. Trap `EXIT` restaurant **tout** même en cas d'échec. Chaque mutation = une vraie erreur de
calcul/structure que le générateur pourrait introduire (cible re-vérifiée unique sur l'arbre courant
AVANT via `grep -Fc`), **jamais** un octet d'artefact ni un commentaire :

| # | Suite (module) | Mutation *generator-logic* injectée | Verdict |
|---|---|---|---|
| 1 | `crm-commissions` | `finance.py` : commission `base * taux` → `base + taux` | **BITES** exit=1 |
| 2 | `pie-manifest` | `pielib/builder.py` : `_statut` inversé (`gated`↔`a_construire`) → compteurs faussés | **BITES** exit=1 |
| 3 | `fiscal-ecf` | `ecflib/ncf.py` : e-NCF `zfill(10)` → `zfill(9)` (rompt le format 13 car. Ley 32-23) | **BITES** exit=1 |
| 4 | `rbac-roleprofile` | `profilelib/builder.py` : `nb_roles = len(role_names)` → `+ 1` | **BITES** exit=1 |
| 5 | `seo` | `seolib/keywords.py` : dédup neutralisée (`if key in seen and False:`) → doublons | **BITES** exit=1 |
| 6 | `legal/confotur` | `cflib/builder.py` : options `entites` jointes `"\n"` → `"|"` (mauvais séparateur Frappe) | **BITES** exit=1 |

**Résultat : 6 BITES · 0 TOOTHLESS.** Chaque suite échantillonnée oppose au merge une dérive réelle
de la **logique** de son générateur — aucune n'est un simple miroir d'artefact. Combiné à hier,
l'opposabilité au merge est prouvée sur **8 gates statiques + 6 suites de module**.

**Portée honnête (pas de plafond silencieux).** Échantillon de **6 des 25 suites** de module,
choisi pour la diversité des styles d'assertion (calcul monétaire · compteurs de statut · format
réglementaire · métadonnée de comptage · dédup · sérialisation d'options). Les **19 suites non
mutées ce jour** (`publiciste`, `rbac`(agrégée)/`fixtures`/`userperm`/`applyplan`, `faisabilite-gen`,
`bancable`, `workflow-vente`, `dossier-vente`, `financement-bancaire`, `portails`, `audit-5d`,
`chat-otoia`, `audit-4big`, `demo-scenario`, `regression`, `deploy-runbook`, `acceptance`, `mobile-app-config`)
restent à attester en session ultérieure — non couvertes ≠ édentées, juste non prouvées aujourd'hui.

**Discipline.** Attestation **read-only par conception** (harnais en `/tmp`, mutations restaurées
`cp` depuis backup + `cmp` byte-exact · **jamais `git clean`/`git checkout`** — cf. interdit
CLAUDE.md · [[ci-gate-verification-method]]). <!-- ci-allow : mention de l'interdit, jamais exécuté -->
**0 gate ajouté** (#5 — on ne teste pas, on *prouve les tests existants*) · **0 chiffre inventé**
(#6) · **0 édition de production** (seul ce journal change). Axe **dents des suites de module**,
distinct de l'axe **dents des gates** (`031134`) et des attestations de **contenu**.

**Vérifications.** `git status --porcelain` **vide** (arbre byte-pristine post-harnais) · `cmp`
backup↔restauré pour les 6 fichiers touchés = identiques · `./run_ci.sh` → **33 PASS · 0 FAIL · 0
SKIP** (inchangé). Aucune commande VPS (#8) · aucun `git clean`. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 031134 · Attestation launch-readiness des DENTS du gate — mutation-test des **8 gates statiques** (chacun doit sortir NONZERO sur sa dérive) → **8 BITES · 0 TOOTHLESS** · arbre byte-pristine · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**,
arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (8 sprints · 7
critères ✅). Les ~15 dernières sessions ont saturé les axes d'audit *contenu* : cross-artefact,
dérive free-text (docstring↔code, inventaire↔fs), attestation des 4 chiffres stakeholder, des 18
citations `file:line` du registre. **Axe distinct et non couvert en suite depuis le 2026-08-02** :
prouver que les gates ont encore des **DENTS**. `run_ci` prouve qu'ils **PASSENT** sur arbre
propre — il ne prouve **jamais** qu'ils **ÉCHOUENT** sur une vraie dérive. Un gate devenu édenté
(toujours vert) est **pire** qu'absent : un faux vert de launch-readiness. La mémoire
[[guard-constraints-flags-usage-not-mention]] notait la dernière suite-mutation-test à **7/7
ROUGE le 2026-08-02** — or depuis : **+1 gate** (8ᵉ `check-mobile-workflow`, `001109`) et **~3
jours d'éditions**. Aucune session n'a re-prouvé la suite **entière** à 8. Valeur honnête du jour :
**Sprint 8 « regression tests exhaustifs »** — attester les dents des 8, corriger tout édentement.

**Méthode (discipline [[verify-uncovered-before-gating]] appliquée à l'inverse — prouver que la
protection MORD).** Harnais unique hors dépôt (`/tmp`, jamais commité), par gate :
`backup→mutation ciblée→run gate→assert exit≠0→restore byte-exact`, avec trap `EXIT` restaurant
**tout** même en cas d'échec. Une mutation = l'exacte classe de bug que le gate est censé
opposer au merge (aucune liste en dur — chaque cible re-vérifiée sur l'arbre courant AVANT) :

| # | Gate | Mutation injectée | Verdict |
|---|---|---|---|
| 1 | `guard_constraints` | usage chemin interdit `/var/www/html/static/…` (écriture · #4) | **BITES** exit=1 |
| 2 | `validate_json` | JSON malformé (`GARBAGE_NOT_JSON` appendé à un `out/*.json`) | **BITES** exit=1 |
| 3 | `check_docs` | lien Markdown interne cassé (`./inexistant-xyz-123.md`) | **BITES** exit=1 |
| 4 | `check_artifacts` | `out/MANIFEST.json` commité ≠ build frais (1 octet) | **BITES** exit=1 |
| 5 | `check_regression` | `regression_run.json` `passed:25→24` vs run frais | **BITES** exit=1 |
| 6 | `check_ci_integrity` | 1 job (`pie-manifest-tests`) retiré de `gate.needs` | **BITES** exit=1 |
| 7 | `check_readme_claims` | compte d'agents dérivé `13→12` dans README | **BITES** exit=1 |
| 8 | `check_mobile_workflow` | `secrets.` réintroduit dans un `if:` de job (SKIP silencieux) | **BITES** exit=1 |

**Résultat : 8 BITES · 0 TOOTHLESS.** Chaque gate statique oppose encore au merge la classe de
dérive qu'il déclare protéger — **aucun édenté** malgré 3 jours d'éditions. La mutation #8 est
précisément l'anti-pattern `234104` (contexte `secrets` indisponible en `jobs.<id>.if` → build
SKIP silencieux le jour où Michel fournit `EAS_TOKEN`) : opposabilité au merge **confirmée**.

**Discipline.** Attestation **read-only par conception** (harnais en `/tmp`, mutations restaurées
`cp` depuis backup · **jamais `git clean`/`git checkout`** — cf. interdit CLAUDE.md · [[ci-gate-verification-method]]). <!-- ci-allow : mention de l'interdit, jamais exécuté -->
**0 gate ajouté** (#5 — on ne teste pas, on *prouve les tests existants*) · **0 chiffre inventé**
(#6) · **0 édition de production** (seul ce journal change). Distinct des attestations
`234104`/`004112` (contenu) : axe **dents des gates**, jamais couvert en suite complète à 8.

**Vérifications.** `git status --porcelain` **vide** (arbre byte-pristine post-harnais) · `diff`
backup↔restauré pour les 8 fichiers touchés = identiques · `./run_ci.sh` → **33 PASS · 0 FAIL · 0
SKIP** (inchangé). Aucune commande VPS (#8) · aucun `git clean`. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 024134 · Attestation launch-readiness du `OPEN_DECISIONS_REGISTER` (18 citations `file:line` + 2 numériques d'en-tête re-prouvées) → 1 FIX de dérive latente : dé-figeage des 2 compteurs mobiles gelés dans le doc · 0 gate ajouté · 0 édition d'autorité

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre, roadmap intégralement livrée/gatée. Le `OPEN_DECISIONS_REGISTER.md` a été **promu
découvrable hier** (session `021124`) mais son contenu — **18 citations `file:line` vers des
sources d'autorité** (`CLAUDE.md`, directives datées, `pie_spec.json`, `gate.py`, journaux) —
n'avait **jamais été re-vérifié indépendamment** contre les numéros de ligne courants. Un
punch-list *launch-readiness* dont les ancres pointent à côté est trompeur. **Valeur honnête
distincte** : attester chaque citation, et corriger toute dérive réelle du doc lui-même (le
registre est **notre** doc worker, pas une source d'autorité — l'éditer pour l'exactitude est
un travail worker légitime, distinct de *SURFACE-don't-rewrite* qui protège les autorités).

**Attestation (read-only d'abord).** Les **18** citations re-prouvées exactes au numéro de ligne :
- D-01 (PIE) : `pie_spec.json:24` (`contrats`→`legal/confotur`) · siblings `null` `:22,27,28,29,30` ·
  `out/MANIFEST.json:24` · `README.md:48` · `out/pie_manifest.json:165-166` · `legal/confotur/README.md:8-17`. **Toutes exactes.**
- D-02 (financement) : `DIRECTIVE_FINANCEMENT…:206-210` + `:275-279` (couche `## PRÉCISION` audit-IA) ·
  `finlib/gate.py:142-145` (`_cond_validation_wag` teste encore `wag_validated_by`) · `financement_spec.json:157`. **Toutes exactes.**
- D-03/D-04 (inventaire) : `AGENTS_EXISTING_ASSETS.md:102/127/128` · `CLAUDE.md:28` · `activity_log/2026-08-03.md:1245-1250`/`:1251-1253` · filesystem (`chat.py`, `projets_editor.py` absents). **Toutes exactes.**
- D-05 (mobile) : `mobile_spec.json` (`a_confirmer`) · `out/app_config.json` (`bundleIdentifier`/`package` `null`) · `DIRECTIVE_MOBILE_STORES:15-16` (`com.otov7.app`). **Toutes exactes.**

**FIX — dérive latente réelle (en-tête, ligne 9).** Le `> **Portée**` gelait **deux compteurs
activement mobiles** : `33/33 gates` et `24 modules 4Big 100/100`.
- `33` : le compte de `gate.needs` est passé **32→33 il y a 2 jours** (`b7ffb64`) ; le README
  **refuse délibérément** de le figer (« `gate.needs` dérivé de ci.yml, *sans liste en dur* »,
  `README.md:28`). Le registre était le **seul** doc à le geler → à contre-courant de la discipline projet.
- `24` : historique `21→22→24` (commentaires de `check_readme_claims.sh`) ; ce chiffre est **déjà
  gaté** (recomputé depuis `quality_report.json` totals pour le README + `audit_4big/README.md`).
  Le registre en était une **3ᵉ copie ungated** → dérive silencieuse au prochain module ajouté.
- **Correction** (dé-figeage, pas gate #5) : reformulé en qualitatif — « tous les jobs de
  `gate.needs` passent · tous les modules 4Big au seuil `100/100`, verdict `PASS` » + **pointeur**
  vers les sources dérivées-et-gatées (`README ## État courant` + `quality_report.json`). Aligne le
  registre sur la philosophie *no-hardcoded-count* du README ; discipline single-source, **pas** un gate redondant.

**Discipline.** Attestation confirmée AVANT tout edit (33/33 gates · 24 modules `100/100`/`PASS`
re-dérivés : `gate.needs`→33 · `quality_report.json` totals→`{modules:24, pass:24, min/max:100,
verdict:PASS}`). **0 édition d'autorité** (`CLAUDE.md`, directive, `pie_spec.json`, `gate.py`
intouchés) · **0 gate ajouté** (#5) · **0 chiffre inventé** (#6, on **retire** deux chiffres, on
n'en ajoute aucun) · **1 seule ligne** du registre modifiée. Items D-01..D-05 **inchangés** (la
provenance « consolidation » reste `014119` — ce reword ne touche aucun item).

**Vérifications.** `grep` compteurs `N/N`/`N modules` résiduels dans le registre → **aucun (clean)**.
`./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. Aucune commande VPS (#8) · aucun `git clean`. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 021124 · `OPEN_DECISIONS_REGISTER` rendu découvrable — indexé dans la Navigation README + pointeur « État courant » (le punch-list launch-readiness n'était référencé nulle part) → 2 éditions README, 0 édition d'autorité

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**,
arbre propre, roadmap intégralement livrée/gatée. La session `014119` (juste avant) a créé
`05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` — une punch-list *launch-readiness* consolidée
« pour que Michel la trouve ». **Mais elle ne la référençait de nulle part** : `grep -rl
OPEN_DECISIONS_REGISTER --include=*.md` ne renvoyait **que** la ligne du journal `014119`.
Le README `## Navigation` indexe pourtant son sibling `GAP_ANALYSIS_SPRINT1.md` ligne 24. Un
punch-list que personne ne peut atteindre est de valeur nulle : **fermer cette boucle de
découvrabilité** est la valeur honnête distincte du jour (ni gate redondant #5, ni
re-surfaçage — le registre consolide déjà).

**Vérification AVANT promotion** (anti-invention #6 · on ne met pas en avant un doc non re-vérifié).
Re-prouvé indépendamment les claims-clés du registre au filesystem/source :
- D-01 : `pie_spec.json:24` attribue bien `contrats` (Promesa · Fideicomiso · HOA) à
  `legal/confotur` — RÉEL.
- D-03 : `CLAUDE.md:28` liste `chat.py` ; `/opt/oto/otoia/capabilities/chat.py` **absent** — RÉEL.
- D-04 : `AGENTS_EXISTING_ASSETS.md:127` « projets_editor.py API en place » ; **aucun**
  `projets_editor.py` sous `/opt/oto/config` — RÉEL (claim runtime VPS non réfutable, #8).
Registre jugé fidèle → promotion légitime.

**Éditions (2, README seul).**
1. `## Navigation` — nouvelle ligne juste après `GAP_ANALYSIS_SPRINT1.md`, pointant le registre
   (description : punch-list décisions produit ouvertes qui appartiennent à Michel · sourcée,
   ne tranche pas).
2. `## État courant (sourcé)` — bullet « Décisions produit ouvertes » : *CI verte ≠ zéro décision
   en attente* → pointe le registre. **Sans compte chiffré en dur** (le nombre d'items dériverait ·
   #6) : pointeur qualitatif uniquement.

**Discipline.** *SURFACE, don't rewrite* : **0 édition** de `CLAUDE.md`, d'une directive datée,
d'un `pie_spec.json`/`gate.py`, ni du registre lui-même. Aucun gate ajouté (#5). Les 2 lignes
README sont de la prose de navigation (pas de claim numérique gaté).

**Vérifications.** `ci/check_readme_claims.sh` → **vert** (chiffres README inchangés, fidèles).
`ci/check_docs.sh` → **vert** (nouveaux liens internes valides). `./run_ci.sh` → **33 PASS · 0
FAIL · 0 SKIP**. **0 chiffre inventé (#6) · 3 claims re-prouvés avant promotion · 0 édition
d'autorité · aucune commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 014119 · Registre des décisions ouvertes — consolidation sourcée des arbitrages produit surfacés au fil de l'eau (Sprint 8 launch-readiness) → 1 livrable-doc neuf · 0 édition d'autorité

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**,
arbre propre. Roadmap intégralement livrée/gatée. Les **6 dernières sessions** (`200738`
08-03 → `011114` 08-05) ont saturé l'audit read-only (inter-artefacts, dérive free-text,
attestation stakeholder, 8ᵉ gate mobile). Reproduire une 7ᵉ passe read-only aurait été à
faible valeur / redondant (#5). **Valeur honnête distincte du jour** : les items *réellement
ouverts* (arbitrages de périmètre produit + dépendances runtime hors dépôt) sont **éparpillés**
dans les journaux de session et n'existaient **nulle part de façon consolidée et découvrable**
pour Michel — un vrai trou de *launch-readiness* Sprint 8. J'ai créé
`05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` : une punch-list unique, **100 % sourcée**,
qui **regroupe** (ne tranche pas, ne réécrit aucune source d'autorité).

**Vérification indépendante de CHAQUE signal AVANT consolidation** (anti-invention #6 · deux
Explore parallèles + spot-check manuel). Aucun item repris de mémoire sans re-preuve :
- **D-01 PIE `contrats`→`legal/confotur`** : REAL. `pie/manifest/pie_spec.json:24` attribue
  `contrats` (Promesa · Fideicomiso · HOA) à `legal/confotur` ; or ce module ne produit **que**
  le DocType demande CONFOTUR (`legal/confotur/README.md:8-17`). Incohérence interne confirmée :
  tous les autres downstreams sans générateur portent `"module": null` (`pie_spec.json:22,27-30`),
  `contrats` est le seul attribué à un module qui ne le génère pas. Arbitrage de périmètre.
- **D-02 Financement condition #4 WAG→audit IA** : REAL. Directive `## PRÉCISION`
  (`DIRECTIVE_FINANCEMENT_..._20260803.md:275-279`) supersede `wag_validated_by` par
  `audit.decision=='APPROVED'` signé ; le module ship encore l'ancienne couche
  (`finlib/gate.py:142-145` + `financement_spec.json:157`). Dépend d'un agent auditeur hors dépôt.
- **D-03 `chat.py` absent** / **D-04 `projets_editor.py` absent** : REAL, filesystem confirmé
  (ni `.py` ni `.pyc` sous `/opt/oto`), **déjà surfacés** session `20260803_133718`
  (`2026-08-03.md:1245-1253`) → registre = **pointeur** anti-doublon (#5), pas re-surfaçage.
- **D-05 bundle id mobile** : soupçon INVALIDÉ → 🟢 VÉRIFIÉ-SANS-SUITE. Le renommage
  OTOV7→« OTO Enterprise OS » ne touche pas le bundle : `bundleIdentifier`/`package` quarantinés
  `null · a_confirmer`, découplés du nom d'app (`mobile_spec.json`, `out/app_config.json`).
  Consigné pour ne pas ré-ouvrir. Faux-positif écarté (#6).

**Discipline.** *SURFACE, don't rewrite* respecté : **0 édition** de `CLAUDE.md`, d'une directive
datée, ou d'un `pie_spec.json`/`gate.py` (les toucher serait invention de périmètre ou réécriture
d'autorité). Le registre est un doc-pointeur volontairement non-gaté (chaque ligne cite sa source
file:line, contenu 100 % dérivé — faible surface de dérive), au même titre que `daily_reports`.

**Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier gaté
touché). **0 chiffre inventé (#6) · 1 faux-positif écarté (bundle id) · 4 signaux re-prouvés
indépendamment avant consolidation · 0 édition d'autorité · aucune commande VPS (#8) · aucun
`git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 001109 · Nouveau gate `check-mobile-workflow` — la structure de `mobile-build.yml` devient opposable au merge (surface mutation-testée UNGATED, 1er contrôle) → 1 gate ajouté + câblé · 32→33 jobs

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0
SKIP**, arbre propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée
(8 sprints). Les 4 sessions du 08-04 (`221055`, `224101`, `231101`, `234104`) avaient
saturé les surfaces d'audit read-only (inter-artefacts, dérive non-gatée, attestation
stakeholder) puis **durci `mobile-build.yml`** (pattern preflight-output, portable
GitHub/Gitea). Cette dernière édition a laissé une **queue** : son propre log notait
« Aucun gate ne linte la structure de `mobile-build.yml` (`check-ci-integrity` ne
verrouille QUE `ci.yml`) ». Autrement dit le correctif de portabilité qu'elle venait
de poser **pouvait régresser en silence** — exactement la classe « marche dans ma
tête, casse le jour où on en a besoin » (mémoire [[mobile-build-secrets-if-gating]] :
« workflow HORS `gate.needs` → `run_ci` won't catch a regression »). La valeur honnête
du jour : **fermer cette queue** en rendant la structure de `mobile-build.yml`
opposable au merge. NON redondant (#5) : c'est le **premier** contrôle de ce fichier.

**Preuve d'absence de couverture AVANT d'ajouter le gate** (discipline
[[verify-uncovered-before-gating]]). Mutation : réintroduire l'exact anti-pattern
corrigé en `234104` — `secrets.EAS_TOKEN != ''` dans un `if:` au niveau job. Résultat :
**7/7 gates statiques restent VERTS**. Surface confirmée non gatée (ni
`check_ci_integrity` — scope `CI=ci.yml` — ni `validate_json` — scope `.json`). Fichier
byte-restauré aussitôt (`cp` depuis backup tmp · jamais `git clean`). <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**Livrable : `ci/check_mobile_workflow.sh` (8ᵉ gate statique) + câblage.** Le gate lit
des **FAITS dans le fichier** (jamais une liste à la main · #6), 4 invariants :
- **MOB-1 · bien-formé** — existe · `name:`/`on:`/`jobs:` + 3 jobs (`preflight`,
  `build-ios`, `build-android`) ; parse YAML strict **exigé si PyYAML dispo**, sinon
  awk seul (runner sans pip → best-effort, jamais un faux vert : MOB-2/3/4 en sont
  indépendants).
- **MOB-2 · portabilité** — **0** `secrets.` dans un `if:` de **job** (`^    if:`).
  Le contexte `secrets` n'est pas exposé dans `jobs.<id>.if` (table d'availability
  GitHub Actions) → sur `act`/Gitea il s'évaluerait vide → **SKIP silencieux**. C'est
  la régression `234104` verrouillée.
- **MOB-3 · activation différée (#6/#8)** — chaque job de build (`run: eas build`) est
  gaté sur `needs.preflight.outputs.has_token` → SKIP tant que le token absent, donc
  le workflow ne rend jamais le CI rouge avant activation Michel.
- **MOB-4 · contrat d'outputs** — `preflight` déclare `has_token`/`has_repo`, chaque
  `needs.preflight.outputs.<X>` référencé est **déclaré** (0 dangling) et chaque output
  déclaré est **alimenté** par un `echo …>>$GITHUB_OUTPUT`.

**Bug corrigé pendant l'écriture (auto-vérification).** 1er jet : `preflight` était
classé « job de build » car un **commentaire** `# … eas build …` (l.108, entre deux
jobs) était attribué au job précédent par le parseur awk. Corrigé en exigeant
`run:[[:space:]]*eas build` (commande réelle, pas une mention). Après correctif : gate
**VERT** sur le fichier propre.

**Mutation-testing du gate lui-même (5 mutations · toutes ROUGES, fichier propre
re-VERT).** (1) YAML cassé (indent) · (2) `secrets.` en `if:` de job · (3) `if:` du
job `build-ios` supprimé · (4) output fantôme référencé (`has_tokenX`) · (5) output
`has_repo` non alimenté. Chaque mutation → `exit 1` ; `cp` backup → gate re-VERT.

**Câblage (garde `check-ci-integrity` INV-A/B vert).** Job `check-mobile-workflow`
ajouté à `ci.yml` (steps `bash ci/check_mobile_workflow.sh`) **et** à `gate.needs`.
`check_ci_integrity` reconnaît le nouveau `ci/*.sh` comme **câblé** (lancé par un job
du gate + il source `lib.sh`) → INV-A (gate.needs == tous jobs non-manuels) et INV-B
(tout `ci/*.sh` câblé) **restent verts**. `run_ci.sh` (dérivé de `gate.needs`, sans
liste en dur) l'inclut **automatiquement** → passage **32→33 jobs (8 gates statiques
+ 25 suites)**, zéro édition de `run_ci.sh` (#6, exactitude structurelle).

**Doc (traçabilité, pas de dérive).** `ci/README.md` : ligne §1 pour
`check-mobile-workflow`, sous-section §2 détaillée (4 invariants + les 5 mutations),
et nuance ajoutée à « Second workflow » (les **jobs** restent hors gate de merge, mais
la **structure du fichier** est désormais gatée). Convention [[ci-readme-table-detail-in-section2]]
respectée : ligne §1 courte, détail en §2. Les mentions historiques « 7 gates
re-verts » (§2, records de mutations passées) restent exactes en **passé** ; le nouveau
record dit **8ᵉ gate**.

**Vérifications.** `bash ci/check_mobile_workflow.sh` → VERT (4 invariants) ; 5
mutations → ROUGE ; `python3 yaml.safe_load` sur `ci.yml` → valide ; `check_ci_integrity`
→ VERT (câblage intègre) ; `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. **1 gate
ajouté (NON redondant : 1re couverture d'un fichier mutation-testé ungated · pas un
doublon #5) · 1 job CI câblé · 0 nouveau module · 0 chiffre inventé (#6) · 0 édition
de production hors CI · aucune commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 004112 · Currency du canal stakeholder `daily_reports` au jalon 33 jobs + réattestation indépendante → CLEAN · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0
SKIP**, arbre propre. Roadmap intégralement livrée+gatée. La session `001109` (même
jour, plus tôt) a fait franchir au gate de merge le cap **32 → 33 jobs** (8ᵉ gate
statique `check-mobile-workflow`). Or le canal roadmap-facing `daily_reports/` s'arrêtait
au `2026-08-04` en affichant encore « **32 PASS** ». Les surfaces de dérive read-only
étant saturées (re-scan = redondant #5), la valeur honnête du jour est de **rendre le
canal stakeholder fidèle au point courant** — discipline [[two-logging-channels]] :
garder les deux canaux, chaque figure sourcée d'un artefact commité.

**Vérification préalable — pas de dérive présent-tense « 32 » résiduelle.** Balayage des
surfaces present-tense (`README.md`, `ci/README.md`) : **aucun** décompte de jobs figé en
dur (le nombre est **dérivé à l'exécution** par `run_ci.sh --list`, jamais retranscrit).
Les nombreux « 32 » des logs/daily_reports antérieurs sont des **snapshots datés** (vrais
à l'époque) → classe *historical-repro = KEEP* ([[prose-facts-vs-numeric-drift]]), on n'y
touche pas. Le « 7 gates statiques » de `ci/README.md` l.49 décrit l'état **avant** l'ajout
du 8ᵉ gate (narratif du mutation-test préalable) → exact en passé, KEEP.

**Livrable : `05_deliverables_mvp/daily_reports/2026-08-05.md`.** Snapshot stakeholder
consignant le jalon 33 jobs (= **8 gates statiques + 25 suites**) + réattestation. Chaque
chiffre **recomputé `python3` indépendamment** des artefacts commités (aucune valeur saisie
à la main · #6), tous concordants avec la table :
- `quality_report.json` : **24/24 modules == 100** · `coverage.ok=true` · ci==registry==24 · verdict `PASS`.
- `regression_run.json.totals` : **24 suites · 624 ran · 624 passed · 0/0/0** · verdict `PASS`.
- `acceptance_matrix.json` : **15/15 `status=in_repo`** · verdict `True` · **`ci_job` ×36** (niché `matrix[].modules[]`).
- Structure CI : `run_ci.sh --list` = **33 jobs = 8 statiques + 25 suites** · `ls ci/*.sh` (hors `lib.sh`) = **8**.

**Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — livrable
prose, hors `check_artifacts`/`audit_4big`). Liens Markdown du rapport résolus depuis
`daily_reports/` : `../qa/…` (artefacts), `../../run_ci.sh`, `../../.gitea/…`, `../../ci/…`
→ `check_docs` VERT. **0 chiffre inventé (#6) · 0 édition de production · aucune commande
VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 011114 · Double sweep read-only sur les DEUX classes de dérive free-text ungated (docstring↔code des 22 générateurs · inventaire↔filesystem) → CLEAN · 0 édition de production · faux « nouveau finding » écarté (anti-invention #6)

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0
SKIP**, arbre propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée+gatée
(8 sprints · 7 critères de succès ✅). Les deux sessions du jour ont déjà (a) ajouté le
8ᵉ gate statique `check-mobile-workflow` (`001109`, 32→33 jobs) et (b) mis le canal
stakeholder `daily_reports` au jalon 33 jobs avec réattestation des 4 chiffres (`004112`).
Les surfaces d'audit inter-artefacts / dérive gatée / attestation sont **saturées**
(re-scan = redondant #5). La valeur honnête restante : re-vérifier les **deux classes de
dérive free-text NON gatées** que la mémoire marque comme récurrentes/non-fermées, avant
d'envisager quoi que ce soit — discipline [[verify-uncovered-before-gating]] (prouver
l'état AVANT d'agir) + anti-invention #6.

**Sweep 1 — docstring↔code des 22 générateurs ([[docstring-vs-code-drift]], classe
RECURRING/not-closed).** Pour chacun des 22 `*_gen.py` de `05_deliverables_mvp/*` :
comparaison du **docstring/en-tête** (inputs annoncés · fichiers `out/` produits ·
fallbacks) au **code réel** (argparse · `open(...,"w")` / `json.dump` / `write_text` ·
branches de fallback). Résultat : **22/22 CLEAN** — chaque docstring décrit exactement les
sous-commandes, les inputs et les artefacts écrits. Spot-check manuel des deux générateurs
multi-sorties les plus risqués (au-delà de la lecture agent) :
- `demo/scenarios/demo_scenario_gen.py` — docstring l.20/l.32 annonce `run_sheet.json` +
  `run_sheet.md` + `MANIFEST.json` → code écrit bien les 3 (l.197-199 JSON, l.203-204 `.md`). ✓
- `faisabilite/bancable/bancable_gen.py` — docstring `50_financier_bancable/{fr,en,es}.md`
  → écrits l.171-173. ✓
  Aucune régression du type « fallback §5.3 fantôme » (publiciste) ni « output omis »
  (demo run_sheet.md) qui avaient été corrigés antérieurement : la classe reste propre.

**Sweep 2 — inventaire `AGENTS_EXISTING_ASSETS.md` ↔ filesystem `/opt/oto`
([[inventory-vs-filesystem-drift]]).** Extraction de tous les chemins `/opt/oto/*` cités
(38 uniques), classification **SRC-OK / PYC-ONLY / MISS** (avec check `__pycache__`) :
- **8 PYC-ONLY** (`oto_agent_faisabilite`, `oto_orchestrateur_faisabilite`,
  `oto_module_{etudes_faisabilite_real,execution_faisabilite,faisabilites_pages,faisabilites_viewer,mobile_api,plans_ifc}`)
  → **tous** déjà annotés « (compilé pyc) » dans le doc : convention interne **cohérente**,
  0 dérive.
- **2 MISS** : `otoia/capabilities/chat.py` (absent vs CLAUDE.md §Architecture cible qui le
  liste) et `config/projets_editor.py` (§13, « API GET/POST déjà en place » — sibling
  `projets_config.json` présent, lui). **Vérification anti-invention #6 AVANT de flaguer :**
  `grep -rn` interne → **les deux MISS sont DÉJÀ surfacés à Michel** par la session `133718`
  du **2026-08-03** (log l.1245/1284/1289-1295 : claims VPS-externes / non-réfutables laissés
  ouverts, chat.py ≠ le module chatbot-lead distinct). Donc **PAS un nouveau finding** —
  re-signaler serait redondant (#5). Le reste (containers Docker, Speckle, systemd, GSC) est
  runtime, non réfutable depuis le dépôt → hors périmètre worker (#8). **SURFACE, don't
  rewrite** respecté : 0 édition de l'inventaire (le fichier peut viser un runtime
  container-side ; un « fix » unilatéral serait une invention).

**Conclusion honnête.** Les deux seules classes de dérive free-text ungated sont **stables
et propres** à ce jalon ; la dimension chiffres-README est densément gatée (`check_readme_claims`
= 27 claims / 16 modules + fiches). Rien à corriger, **aucun gate à ajouter** (ajouter serait
un doublon #5 ; par ailleurs un gate lisant `/opt/oto` serait non-portable — le runner Gitea
n'a pas ce filesystem, cf. [[ci-gate-verification-method]]). Session **read-only** volontaire,
dans la lignée des attestations `234104`/`004112`, mais sur un **axe distinct** (les 2 classes
free-text, pas les 4 chiffres stakeholder ni le cross-artefact).

**Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier
gaté touché). **0 chiffre inventé (#6) · faux nouveau-finding écarté par grep interne · 0
édition de production · 0 gate ajouté (anti-redondance #5) · aucune commande VPS (#8) · aucun
`git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 054200 · Currency du canal STAKEHOLDER : consigner la clôture des DEUX axes de « dents » dans `daily_reports/2026-08-05.md` · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**,
arbre propre. Roadmap intégralement livrée+gatée côté repo (Sprint 8 buffer). Les surfaces
de dérive usuelles sont saturées, donc j'ai d'abord **cherché une vraie régression**, pas une
attestation redondante (#5) :

1. **Chasse docstring↔code drift** (classe documentée RÉCURRENTE/non-close — a déjà trouvé le
   « fallback §5.3 fantôme » publiciste + l'output `run_sheet.md` omis). Balayage des **21
   générateurs** (docstring/commentaires vs fichiers réellement écrits : `open`/`write_text`/
   `json.dump`). **Résultat : 0 contradiction réelle.** Les « écarts » remontés (docstrings
   sans liste-de-sorties en puces) étaient des **faux positifs** : `commissions_gen.py` documente
   bien ses sorties dans le bloc *Sous-commandes* (`build → écrit commission_plan.json +
   MANIFEST.json`), simple placement différent de `financement` (7 sorties → liste séparée).
   Uniformiser serait de la **pure churn** (#5) — écarté.

2. **Seule action honnête + non-redondante trouvée : currency du canal stakeholder.** Le
   `daily_reports/2026-08-05.md` (session `004112`, matin) s'arrête au jalon **33 jobs** et
   **précède** toute la série d'attestation *teeth* des **suites de MODULE** (BATCH 1→4,
   commits `034144`→`051154`, clôture **25/25**) jouée plus tard le même jour. Le canal
   stakeholder ([[two-logging-channels]]) ne captait donc **aucun** des deux axes de « dents ».

**Fait.** Ajout d'une section « Mise à jour fin de journée » au rapport du jour, consignant la
**clôture des deux axes** : (A) **8/8** gates statiques (`9fe8bf1`/`031134`) · (B) **25/25**
suites de module (`6+6+6+7`, `98f3f56`/`d25da28`/`e38dd93`/`986d293`). Chaque chiffre **sourcé
sur une structure commitée** (`ls ci/*.sh` hors `lib.sh` = 8 · `gate.needs` = 25 · série de
commits) — **0 saisi à la main** (#6). Message stakeholder explicité : les tables de currency
prouvent que la CI est **verte** ; les attestations *teeth* prouvent qu'elle **mord** (un gate/
suite toujours-vert = faux vert invisible aux tables).

**Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (seul fichier touché =
`daily_reports/2026-08-05.md` + ce log ; aucun fichier de code gaté). **0 chiffre inventé
(#6) · faux findings docstring écartés · 0 édition de production · 0 gate ajouté (#5) · aucune
commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

## Session 061201 · Sweep REPO-WIDE de la classe « chemin fantôme en code-span backtick » (ungated, documentée récurrente) → 0 fantôme réel · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre,
roadmap intégralement livrée+gatée (Sprint 8 buffer). Plutôt qu'une n-ième attestation
redondante (#5), j'ai d'abord voulu **fermer un axe encore incomplet**. Deux axes de câblage
CI vérifiés d'abord et confirmés **structurellement clos** : (a) parité local↔CI —
`run_ci.sh` **dérive** sa liste de `gate.needs` (aucune liste en dur), et
`check_ci_integrity.sh` verrouille `ci.yml` comme source de vérité ; (b) **gate orphelin** —
`check_ci_integrity` INV-B itère `git ls-files 'ci/*.sh'` et exige que chaque script soit
lancé par un job du gate **ou** sourcé par un gate lancé → un `ci/*.sh` mort/décâblé casse la
CI. Rien à ajouter (#5).

**Axe réellement balayé (nouveau périmètre).** La classe **« chemin in-repo fantôme dans un
code-span backtick »** est **ungated à dessein** (les backticks sont neutralisés par
`check_docs` — cf. [[backtick-path-escapes-check-docs]] ; « ne pas gater tous les backticks »).
L'audit d'exactitude des fiches ([[fiche-accuracy-audit-closed]]) ne couvrait que les **13
`03_agents/*/AGENT.md`**. Je l'ai **étendu à TOUT le markdown suivi** (~200 fichiers :
READMEs de modules, `daily_reports/`, `04_roadmap/`, `ci/README.md`, DIRECTIVE_*, GAP_ANALYSIS,
logs) via un harnais qui extrait chaque token backtické contenant un `/` et le résout contre
**quatre ancres** : dossier du doc, racine du dépôt, `05_deliverables_mvp/`, et chaque
sous-module `05_deliverables_mvp/*/`.

**Résultat : 0 fantôme réel.** Les ~180 tokens remontés se ventilent en **trois formes
bénignes**, chacune vérifiée sur échantillon :
1. **Runtime/VPS hors-dépôt** (`/static/`, `otoia/capabilities/*`, `www/`, `seo_autonome/`,
   `data_room/PXX/`) — non réfutables depuis le dépôt (#8), déjà connues.
2. **Citations cross-module en prose** — un README/rapport nomme le fichier d'un AUTRE module
   par son nom court. Vérifié : `crm/commissions/README.md` cite `banclib/finance.py` **à
   l'intérieur d'un vrai lien Markdown** dont la **cible existe et EST validée par
   `check_docs`** (le backtick n'est que le libellé) ; `banclib`/`wflib`/`cflib` résolvent tous
   dans le dossier de leur module propre.
3. **Narratif d'historique + non-chemins pédagogiques** — les `05_activity_log/*` antérieurs
   citent des fichiers par nom court (prose, pas navigation), et certains chemins **n'existent
   pas EXPRÈS** : `./inexistant-xyz-123.md` / `./this/does/not/exist.md` sont les liens cassés
   d'exemple prouvant que `check_docs` **mord**, et `/tmp/suite_teeth_*.sh` sont les harnais de
   mutation-test éphémères (`/tmp` read-only). Correct par conception.

**Conclusion honnête.** L'axe fantôme-backtick est **propre repo-wide** (extension du 13→~200
`.md`), les deux axes de câblage CI sont clos, les axes *teeth* (8/8 statiques · 25/25 suites)
et free-text/chiffres étaient déjà saturés. **Aucun défaut réel trouvé → 0 édition de
production** (churner un doc exact serait de la dérive #5) et **0 gate ajouté** (gater les
backticks est explicitement rejeté, et un gate lisant `/opt/oto` serait non-portable côté
runner Gitea — cf. [[ci-gate-verification-method]]). Seule sortie : ce log + une note mémoire
pour que la prochaine session ne rejoue pas ce sweep à froid ni ne re-flague ces formes.

**Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (seul fichier touché = ce
log ; aucun fichier de code gaté). **0 chiffre inventé (#6) · faux positifs backtick écartés
par résolution multi-ancre + inspection · 0 édition de production · 0 gate ajouté (#5) ·
aucune commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 20260805_064201 · FIX RÉEL — la fenêtre de sprint *roadmap-globale* de `faisabilite/generator` était fausse (S2 → S3)

**Défaut trouvé (première vraie édition de production depuis plusieurs sessions de sondes « 0 édition »).**
Le registre de l'auditeur 4Big attribuait au **générateur de faisabilité 4 volets** une fenêtre
de sprint **roadmap-globale erronée** :

- `05_deliverables_mvp/qa/audit_4big/quality_spec.json:66`
  → `"sprint": "S2"`, `"source": "roadmap Sprint 2 · Faisabilité générateur 4 volets …"`.

Or la roadmap **ne contient AUCUNE faisabilité au Sprint 2** (`04_roadmap/…:35-39` = Console /
CRM / RBAC). La **« génération 4 volets <1h »** est explicitement au **Sprint 3**
(`…:43`, livrable `…:46` « P07 Aqua Terra faisabilité complète auto-générée »). La citation
`source` était donc **falsifiable et fausse** (classe [[prose-facts-vs-numeric-drift]] · sous-classe
*citation mal-attribuée* + [[roadmap-anchor-gate]]).

**Cause racine — confusion de deux cadres de « sprint ».**
1. **Cadre agent-interne** (phases de construction par agent) : `03_agents/faisabilite/AGENT.md`
   §130-133 définit S1 = template · **S2 = générateur 4 volets** · S3 = version tracking — le tout
   **ancré à la roadmap Sprint 3** (L41-46). Ce cadre est repris tel quel dans les docs de module
   (`generator/README.md` H1 « Sprint 2 », `TEMPLATE_…:216`, `GAP_ANALYSIS_SPRINT1.md:74-75`,
   `publiciste/README.md:26`, `daily_reports/*`). **Cohérent dans son cadre — laissé intact.**
2. **Cadre roadmap-global** (autorité de la recette) : le champ `sprint` de `quality_spec.json`
   — **toutes** les autres entrées le sourcent « roadmap Sprint N ». C'est le **seul** endroit qui
   doit porter le sprint roadmap-global. Il avait recopié le label agent-interne « S2 ».

**Conséquence corrigée.** Le livrable-phare **S3** « P07 faisabilité complète auto-générée »
était prouvé **sans** le module qui la génère (`faisabilite/generator`), tandis que ce dernier
était compté comme preuve du **S2** (« 7 dashboards · CRM · RBAC ») — auquel il n'a **aucun**
rapport. La preuve la plus directe du livrable-phare était mal-classée.

**Correctif (édition coordonnée 2-spec + régénération byte-gatée).**
- `quality_spec.json:66` : `"sprint"` **S2 → S3**, `source` recitée « roadmap Sprint 3 · Faisabilité
  générateur 4 volets → data_room/PXX » (aligné sur le style de son jumeau `bancable`, ligne 71,
  déjà S3).
- `acceptance_spec.json` : `faisabilite/generator` **retiré** de `evidence_modules` S2, **ajouté**
  à S3 (`["faisabilite/bancable", "faisabilite/generator"]`) — sinon l'**invariant 5 « partition
  exacte »** casse (`want` lu dans `quality_spec` ≠ `have` déclaré). Édition des DEUX specs
  obligatoire et suffisante.
- Régénération : `audit_4big` (report) **puis** `acceptance` (lit la fenêtre depuis `quality_spec`).

**Vérifications.** `partition_ok=True · bijective=True` · S2 = 6 modules (publiciste + 5 rbac,
exact) · S3 = {bancable, generator} (exact). `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.
Score 4Big du générateur **inchangé 100/100** (le champ `sprint` ne sert qu'au tri + méta du
report, pas d'oracle de scoring vérifié dans `q4lib/scoring.py`).

**Portée honnête / anti-churn.** **5 fichiers** touchés (2 specs + 3 artefacts régénérés) + ce log.
**Zéro** doc de module éditée : les ~10 « S2 » du générateur ailleurs sont le **cadre agent-interne**
(ancré Sprint 3), corrects — les churner serait de la dérive #5. `daily_reports/*` = historique,
non réécrit. **0 gate ajouté** (surface déjà gatée par invariant 5 · [[verify-uncovered-before-gating]]),
**0 chiffre inventé** (#6, tout dérivé du roadmap), **0 commande VPS** (#8), **aucun `git clean`**. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 20260805_074202 · FIX de COUVERTURE — le livrable transverse `demo/scenarios` manquait des DEUX fiches co-porteuses

**Classe** : complétude ≠ exactitude ([[fiche-accuracy-audit-closed]]) — même classe que
le FIX précédent (CRM omettait `financement_bancaire`). Méthode : diff
`05_deliverables_mvp/*/` (modules à générateur + tests + `out/`) vs les tables de
livrables des 13 fiches `03_agents/*/AGENT.md`.

**Défaut réel trouvé.** Le module **`05_deliverables_mvp/demo/scenarios/`** — livrable
bel et bien produit (générateur `demo_scenario_gen.py` · **39 tests** · `out/{run_sheet,MANIFEST}` ·
README · déjà gaté par `check_readme_claims` sur son propre README + job CI
`demo-scenario-tests`) — n'était recensé dans **AUCUNE** des 13 fiches agents (grep
`demo|scenario` sur `03_agents/*/AGENT.md` = 0 hit). C'est un livrable **transverse
sans agent propre** : son README le cadre « Sprint 7 (CRM + Faisabilité) » et la roadmap
L68 l'assigne explicitement à **CRM + Faisabilité**. Les DEUX co-porteurs l'omettaient.

**Correctif — 2 fiches, une seule source gatée (anti-duplication #5).**
- **Fiche CRM** (`03_agents/crm/AGENT.md`) : nouvelle sous-section « Livrable transverse ·
  Sprint 7 — scénarios démo », avec table 1-ligne **auto-gatée** (mirroir du précédent
  `financement_bancaire`). La ligne pointe `…/demo/scenarios/README.md` et finit `| 39 |`
  → captée par le `row_re` de `check_readme_claims` (label `\w+/` = `scenarios/`, chemin
  résolu `demo/scenarios` ∈ `plan.suites`) → cellule Tests **recomputée** depuis
  `regression_plan.json`. Framée honnêtement : **pas un livrable CRM propre** mais un
  méta-générateur qui **compose** les livrables amont, co-porté, **ne partage aucune
  source** avec le trio ni le financement.
- **Fiche Faisabilité** (`03_agents/faisabilite/AGENT.md`) : **cross-référence en prose**
  (pas de chiffre redupliqué · #5) vers la fiche CRM + le README du module, mentionnant
  que `bancable` alimente le scénario `S-P07-BANQUIER`.

**Auto-gates confirmés (3 axes indépendants, la ligne CRM franchit chacun).**
1. `Fiche 03_agents/crm/AGENT.md · demo/scenarios — Tests 39 == source (39)` (recompute
   `regression_plan.json`).
2. `Entrée CLI · demo_scenario_gen.py — verbes build|validate == subparsers`.
3. `Traçabilité roadmap · demo/scenarios — citation l.68 == roadmap`.
Job CI `demo-scenario-tests` == ci.yml. `check_docs` : cross-refs `../crm/AGENT.md`
résolus OK.

**Portée honnête / anti-churn.** **2 fichiers** touchés (2 fiches) + ce log. **0 gate
ajouté** (surface déjà couverte par le `row_re` générique des cellules Tests ·
[[verify-uncovered-before-gating]] · [[doc-numeric-claims-gate]]) — la nouvelle ligne
*hérite* du gate existant. **0 chiffre inventé** (#6 · 39 dérivé de la source unique).
Le trio CRM + « Total CRM 81 tests » + le financement laissés **INTACTS** (demo hors-trio,
source distincte). **0 production éditée** (#5), **0 commande VPS** (#8), **aucun
`git clean`**. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 20260805_084202 · FIX RÉEL — « 6 sections » périmé dans les 2 docs d'implémentation du module `financement_bancaire` (le module en livre 7)

**Classe** : prose FACT count périmée, ungated ([[prose-facts-vs-numeric-drift]] ·
[[doc-numeric-claims-gate]]) — distincte des comptes gatés.

**Défaut réel trouvé.** Deux surfaces d'implémentation décrivant le module livré
`crm/financement_bancaire` affirmaient **« 6 sections »** alors que le module en livre
**7** :
- `05_deliverables_mvp/crm/financement_bancaire/README.md:9` — « la structure des 6 sections »
- `03_agents/crm/AGENT.md:47` — « cœur du gate + 6 sections + bannière »

**Preuve du bon compte = 7** (triple-sourcé, byte-gaté) :
- `out/MANIFEST.json` → `counts.sections = 7` (artefact byte-gaté par `check_regression`/`check_artifacts`).
- `financement_spec.json` → bloc `sections` = 7 (`apport_initial`, `info_achat`,
  `choix_banque`, `formulaires`, `exigences`, `autorisations`, `envoi`).
- Le README **lui-même** se contredisait : ligne 49 énumère les **7** sections de
  l'accordéon, ligne 57 « **7 sections** » (annotée « source de vérité = MANIFEST.json »).
  Seule la ligne 9 (prose libre) était restée à « 6 ».

**Origine du « 6 ».** La `DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET` originale (L106)
énumère exactement **6 sections non-gating** (`Info achat / Choix banque / Formulaires /
Exigences / Autorisations / Envoi`), **puis** a été **amendée** pour ajouter la section
gating `apport_initial` en position 1 → **7** au total. La spec + le module ont suivi (7),
mais les 2 phrases de prose d'implémentation avaient gardé le « 6 » d'avant l'amendement.

**Correctif — 2 surfaces d'implémentation, 6→7. Directives laissées INTACTES.**
Les **4** occurrences « 6 sections » restantes vivent dans des **snapshots directive**
gelés (`DIRECTIVE_FINANCEMENT…` L106/L122/L131 + `DIRECTIVE_WORKFLOW_FAISABILITE_V10`
L22/L47) — **input-specs datés de Michel**, jamais réécrits ([[directive-vs-implementation]] ·
`spec = authority, directive = snapshot`). Leur « 6 » est **historiquement exact** au
cadre pré-amendement (énumération des 6 aval). Seules les 2 surfaces décrivant le
**livrable** sont corrigées.

**Aucun gate ajouté (#5).** Un gate structuré sur la ligne « Comptes courants »
recomputerait la ligne 57 (déjà correcte, adossée à MANIFEST) mais **ne mordrait pas**
la prose libre de la ligne 9 — la classe réelle du défaut. Financement reste le seul
module dont `MANIFEST.counts` n'est pas recompté dans `check_readme_claims` ; **SIGNAL
surfacé** (couverture prose ungated), pas d'ajout ce tour pour éviter un gate fragile +
hors-cible. **0 chiffre inventé** (#6 · 7 dérivé de MANIFEST/spec), **0 production éditée**
(#5), **0 commande VPS** (#8), **aucun `git clean`**. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

---

## Session 20260805_101214 · FIX RÉEL — la fiche MOBILE présentait `oto_module_mobile_api.py` comme source alors que seul le `.pyc` existe (annotation « (compilé pyc) » perdue vs sa propre source citée)

**Classe** : `inventory-vs-filesystem-drift` — un claim d'existence d'asset dérive du filesystem VPS réel ; action sanctionnée = **annoter pyc-only selon la convention du doc lui-même** (distincte des claims chat.py/VPS-API qu'on SURFACE sans réécrire → registre D-03/D-04).

**Défaut réel trouvé.** La table « Modules OTOV7 réels refactorés » de `03_agents/mobile/AGENT.md:76`
cite **explicitement** sa source `AGENTS_EXISTING_ASSETS.md §8`, mais la 1re ligne présentait
`/opt/oto/oto_module_mobile_api.py` comme un module source **sans qualificatif**, alors que :
- **Sa source citée** l'annote `(compilé pyc)` : `AGENTS_EXISTING_ASSETS.md:59`
  → `- /opt/oto/oto_module_mobile_api.py (compilé pyc)`. La fiche avait **perdu** l'annotation.
- **Le filesystem confirme** : aucun `.py` à ce chemin ; seul `/opt/oto/__pycache__/oto_module_mobile_api.cpython-310.pyc`
  (13 Ko, root, 2026-07-02) existe. Classé **PYC-only** (vs les 4 autres lignes de la table —
  `oto_module_mobile_download.py`, `deploy_mobile_rbac.sh`, `seed_mobile_rbac.py`, `oto_mobile.js` —
  toutes **SRC présentes**, correctement laissées sans annotation).
- **La convention sœur** confirme le format : `AGENTS_EXISTING_ASSETS.md §9` annote de la même
  façon `- /opt/oto/oto_module_plans_ifc.py (compilé pyc)`.

**Correctif — 1 ligne, 1 fichier.** Ajout de l'annotation `*(compilé pyc)*` à la seule ligne
concernée, alignant la fiche sur (a) sa source citée §8, (b) le filesystem réel, (c) la
convention §9. Les 4 autres lignes (sources réelles) laissées **INTACTES** — pas de sur-annotation.

**Portée honnête / anti-churn.** **1 fichier** touché (1 fiche) + ce log. **0 gate ajouté** (#5) —
la couverture de ce claim d'existence porte sur un chemin **VPS hors-repo** non vérifiable par un
gate dépôt (#8) ; l'annotation aligne la prose sur la source in-repo `AGENTS_EXISTING_ASSETS.md`,
elle-même correcte. **0 chiffre inventé** (#6). **0 production éditée** (#5), **0 commande VPS**
(#8), **aucun `git clean`**. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. <!-- ci-allow : mention de l'interdit, jamais exécuté -->

**Audits négatifs consignés (deux passes Explore + vérifs manuelles, RAS).** Comptes/tests :
les 15+ « N tests/sections/invariants/rôles » restent synchronisés (ex. `ran=624` recomputé =
650 méthodes − 26 `qa.regression` self-test = 624 ✓ · `24 suites` matrice = 25 module-suites
gate.needs − 1 self-test ✓). Docstrings-vs-code : les 25 générateurs déclarent exactement
leurs sorties/verbes CLI. Citations verbatim & « gaté par X » : échantillon roadmap L29/L38/L46/
L51/L63/L73 vérifié exact. Seul défaut concret = l'annotation pyc ci-dessus.

---

## Session 121234 · RÉ-ATTESTATION indépendante du canal stakeholder (daily_report launch-readiness) → **CLEAN** · 0 édition de production

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée+gatée in-repo ; les restes S3/S5/S6 (refactor
BIM `aec.py`, builds EAS mobile, dépôts ONAPI, déploiement prod VPS) sont **hors périmètre worker**
(`CLAUDE.md` #8 · VPS `153.75.250.214`). <!-- ci-allow : mention de l'interdit, jamais exécuté -->
Le daily_report `2026-08-05.md` (sessions 004112+054200) désigne comme prochaine tâche « maintenir la
currency des deux canaux + veille d'intégrité sur surfaces neuves » ([[two-logging-channels]]). Choix :
**re-attester par recompute `python3` indépendant** que **chaque** chiffre publié dans la table
launch-readiness recharge à l'identique depuis les artefacts commités (pas un écho de la table) — les
classes de dérive prose/count/link usuelles étant saturées et gatées (#5, ne pas re-signaler).

**Recompute indépendant — 5 dimensions, toutes concordantes (aucune saisie manuelle · #6) :**

| Dimension | Publié (daily_report) | Recompute ce jour | = ? |
|---|---|---|---|
| Qualité 4Big | 24/24 modules à 100/100 · `coverage.ok=true` · ci==registry==24 · `PASS` | `quality_report.json` : 24 modules, **tous == 100**, `coverage.ok=true`, ci=registry=24, verdict `PASS` | ✅ |
| Régression (run) | 24 suites · 624 ran · 624 passés · 0/0/0 | `regression_run.json.totals` : suites 24 · ran 624 · passed 624 · failures/errors/skipped 0/0/0 · green 24 red 0 | ✅ |
| Recette roadmap | 15/15 `status=in_repo` · 36 `ci_job` → 0 orphelin | `acceptance_matrix.json` : `matrix` len **15** ; **36** refs `ci_job` (24 uniques) · **0 orphelin** vs les 33 vrais jobs `run_ci --list` | ✅ |
| Structure gate | 33 jobs = 8 statiques + 25 suites | `run_ci.sh --list` : **33 jobs** (footer dérivé) = 8 gates `ci/*.sh` (hors `lib.sh`) + 25 suites module | ✅ |
| Miroir local | `run_ci.sh` → 33 PASS · 0 FAIL · 0 SKIP | idem, inchangé | ✅ |

**Contrôle nouveau (léger renfort, non publié auparavant).** Vérification **bidirectionnelle** du
non-orphelinage des `ci_job` de la recette : les **36** références (24 uniques, niché dans
`matrix[].modules[]`) mappent **toutes** vers un job réel de `gate.needs` (0 orphelin dans les deux
sens). Recoupé au décompte du gate lui-même (`ls ci/*.sh` hors `lib.sh` = **8** ; `run_ci --list` = **33**).
**Aucun gate ajouté** — `check_ci_integrity` couvre déjà l'intégrité gate↔artefacts (#5, [[verify-uncovered-before-gating]]).

**Portée honnête / anti-churn.** **0 fichier de production édité** (#5), **0 gate ajouté** (#5),
**0 chiffre inventé/saisi** (#6 — tout dérivé de `json.load`/`ls`/`--list`), **0 commande VPS** (#8),
**aucun `git clean`** (#1). <!-- ci-allow : mention de l'interdit, jamais exécuté --> Le daily_report
est **exact et courant** → laissé intact (l'éditer serait du churn). Seule édition : ce log. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé).

---

## Session `20260805_124241` — FIX test-precision : constante `18` périmée + commentaire faux dans `test_audit_4big.py`

**Surface NEUVE (hors classes saturées).** Chasse ciblée aux surfaces non encore auditées
(le canal usuel README/counts/docstrings/fiches est saturé, #5). Trouvaille réelle, pas un
faux-positif : `05_deliverables_mvp/qa/audit_4big/tests/test_audit_4big.py`
`test_failing_module_forces_global_fail` figeait `bad["totals"]["pass"] = 18 - fail`.

**Le défaut.** Le périmètre audité a grandi à **24 modules** (`totals.modules == 24`, tous
100/100), mais le test gardait l'entier **`18`** codé en dur. Résultat : le rapport « bad »
qu'il fabrique avait `pass=17, fail=1, modules=24` → il déclenchait **INV7** (visé) MAIS
AUSSI **INV8 `pass+fail ≠ modules`** (18≠24) *et* **INV8 `min/max score incohérents`**
(module passé sous 95 alors que `min_score` restait 100). Or son propre commentaire affirmait
« *Le rapport est cohérent en interne mais porte un FAIL → INV7 le signale* » — **affirmation
fausse** : le rapport n'était PAS cohérent en interne. Le test passait quand même car son
assertion (`any("< seuil 4Big")`) restait vraie parmi les erreurs parasites → test **relâché**,
passant pour de mauvaises raisons, avec une constante latente qui pourrit en silence.

**Preuve du diagnostic (recompute indépendant).** Rejeu de l'ancien setup `18` →
`check_invariants` renvoie exactement `[INV7 publiciste …, INV8 pass+fail ≠ modules, INV8
min/max score incohérents]`. La nouvelle assertion `all("INV7" in e)` **échouerait** sur ce
setup → elle a de vraies dents (pas tautologique).

**Le fix (test uniquement, 0 production).** Totaux dérivés du **vrai** `bad["modules"]` au lieu
d'un entier figé :
- `pass = len(modules) - fail` (robuste à tout futur changement de décompte, #6 — aucun
  chiffre saisi à la main)
- `min_score/max_score = min/max(scores recomputés)`

Le rapport « bad » devient **réellement cohérent en interne** → INV8/INV9 ne mordent plus,
**seul INV7** signale le module sous seuil — exactement ce que le commentaire prétend. Assertion
resserrée de `any("< seuil 4Big")` (relâchée) vers `errs non vide` + `all("INV7")` + `any("< seuil
4Big")` : prouve désormais l'**isolation** d'INV7 comme unique déclencheur.

**Portée / anti-churn.** **0 fichier de production édité** (seul un fichier de test), **0
artefact reconstruit** (aucun `out/*.json` touché → pas de rebuild `quality_report`, cf.
[[audit4big-rebuild-after-doc-edits]]), **0 gate ajouté** (#5), **0 chiffre inventé** (#6 —
tout dérivé de `len()`/`min`/`max`), **0 commande VPS** (#8), **aucun `git clean`** (#1). <!-- ci-allow : mention de l'interdit #1, jamais exécuté (l'escape doit être SUR la ligne fautive, scan ligne-par-ligne) -->

**Vérif.** `python3 -m unittest discover -s tests` → **34 tests OK** ; `./run_ci.sh` → **33
PASS · 0 FAIL · 0 SKIP**. Distinct de la classe *toothless-test/inert-mutation*
([[module-suite-teeth-mutation]]) : ici la LOGIQUE testée est saine — c'est le HARNAIS de test
(données figées) qui avait dérivé.

---

## Session 20260805_131244 · FIX régression gate `constraints-guard` (tree committé RED malgré « 33 PASS » annoncé)

**Constat.** Au démarrage, `./run_ci.sh` sur le tree committé (HEAD 127ef6f) → **32 PASS · 1
FAIL** : job `constraints-guard` ROUGE. Le commit 127ef6f annonçait pourtant « 33 PASS CI » —
il a vérifié les gates AVANT d'avoir son propre journal committé (réflexe [[ci-gate-verification-method]]
non tenu : re-jouer les gates sur le tree AVEC le log staged).

**Cause racine — 3e RÉCURRENCE de la sous-classe `aucun git clean`** (cf. <!-- ci-allow -->
[[guard-constraints-log-prose]]), mais NOUVELLE variante. Ligne 1206 :
`… **0 commande VPS** (#8), **aucun \`git clean\`** (#1).` — le mot `git clean` sans marqueur de <!-- ci-allow -->
prohibition (« aucun » n'est pas dans la regex PROHIBITION). Le journaliste précédent AVAIT bien
posé un `<!-- ci-allow -->`… mais sur la ligne 1207 (paragraphe hard-wrappé). Or `scan_forbidden`
fait `grep -in` **ligne par ligne** : l'escape doit être SUR la ligne fautive, pas sur une voisine.
Donc l'échappatoire ne couvrait rien.

**FIX (journal only, 0 prod, 0 artefact).** Escape déplacé/fusionné INLINE sur la ligne 1206 elle-même
(`… (#1). <!-- ci-allow : … l'escape doit être SUR la ligne fautive, scan ligne-par-ligne -->`) et
suppression de la ligne-comment orpheline 1207.

**Vérif.** `bash ci/guard_constraints.sh` → OK ; `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.
`grep -inE 'git[[:space:]]+clean'` filtré PROHIBITION+ci-allow → 0 hit résiduel sur tout le tree.

**Portée / anti-churn.** **0 fichier de production**, **0 gate ajouté** (#5), **0 chiffre inventé**
(#6), **0 commande VPS** (#8). Seule édition : ce journal. Mémoire [[guard-constraints-log-prose]]
enrichie : l'escape `ci-allow`/marqueur est **PAR LIGNE** — un paragraphe wrappé qui sépare le terme
de son marqueur laisse le gate mordre.

---

## Session 20260805_134254 · SWEEP read-only *docstring-vs-code drift* sur 26 modules → **CLEAN** · 3ᵉ récurrence du faux-positif `validate [-o OUT]` refermée

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo ; les restes
S3/S5/S6 (refactor BIM, builds EAS, dépôts ONAPI, déploiement prod VPS) sont **hors périmètre
worker** (`CLAUDE.md` #8). Le daily_report `2026-08-05.md` et le `OPEN_DECISIONS_REGISTER.md`
(D-01→D-05) sont **déjà courants** (sessions 004112 / 014119) — les ré-attester serait du churn
(#5 ; déjà fait 3× ce jour, cf. 121234). J'ai donc visé la **seule classe que la mémoire marque
explicitement `RECURRING / not-closed`** : [[docstring-vs-code-drift]] — docstrings/aides argparse
de modules de PRODUCTION qui avancent un input/fallback/output/flag que le code contredit (ou qui
omettent un output réellement écrit).

**Méthode.** Fan-out `Explore` en lecture seule sur les **26** modules générateurs (`*_gen.py` +
`lib/*.py` + textes argparse embarqués), amorcé pour SKIP le faux-positif connu. Pour chaque
module : docstring/help ↔ ce que le code lit (`json.load`/reads), écrit (`open(...,'w')`/`write_text`/
`json.dump`/noms de fichiers) et enregistre (`add_parser`/`add_argument`). Direction A (advertise
non-implémenté) **et** B (omet un output réel) toutes deux couvertes.

**Résultat — CLEAN.** Aucune dérive réelle sur les 26 modules. L'unique item remonté par l'agent
est **exactement** le faux-positif déjà refermé 2× (session 111224) : `deploy_runbook_gen.py:28`
`validate [-o OUT]  → (re)génère en mémoire, valide vs deploy.schema.json + invariants…`.
Vérifié en relisant la source (L26-29 + L288) : la ligne `validate` **décrit ce que fait `validate`**
(régénère en mémoire, valide) et **ne prétend jamais** que `-o` écrit quoi que ce soit ; `[-o OUT]`
n'est qu'un flag optionnel accepté, en symétrie CLI avec `build`, et argparse l'enregistre
`help="ignoré (compat)"` (`:288`). **Bijection parfaite, self-cohérente → NON-défaut.** Seul `build`
(`:27`) revendique l'écriture, ce que le code fait. **Aucune édition** de la docstring (#5 · la
mémoire ordonne LEAVE/don't-harmonize : la doc est correcte, c'est le lecteur qui la sur-lit).

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit** (aucun
`out/*.json` touché → pas de rebuild `quality_report`, cf. [[audit4big-rebuild-after-doc-edits]]),
**0 gate ajouté** (#5), **0 chiffre inventé** (#6), **0 commande VPS** (#8 — sweep 100 % lecture),
**aucun `git clean`**. <!-- ci-allow : mention de l'interdit #1, jamais exécuté ; escape INLINE sur la ligne fautive (scan ligne-par-ligne, cf. session 131244) --> Seule édition : ce journal.

**Vérif.** `git status --short` avant/après le sweep = vide (lecture seule) ; `./run_ci.sh` →
**33 PASS · 0 FAIL · 0 SKIP**. Mémoire [[docstring-vs-code-drift]] enrichie : classe re-balayée
CLEAN sur les 26 modules ce jour, et le faux-positif `validate [-o OUT]` a **récidivé une 3ᵉ fois**
(re-signalé par un `Explore` neuf malgré l'amorce de SKIP) → confirme qu'il faut le garder documenté
comme piège, pas le "corriger".

---

## Session 20260805_141301 · SWEEP neuf *forward-compat warnings* — 25 suites sous `-W error` → **CLEAN** (classe absente de la mémoire)

**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre
propre (HEAD `48b19b9`). Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo ;
restes S3/S5/S6 (refactor BIM, builds EAS, dépôts ONAPI, déploiement prod) **hors périmètre worker**
(`CLAUDE.md` #8). Les surfaces de dérive usuelles sont saturées et le daily_report `2026-08-05` +
`OPEN_DECISIONS_REGISTER` sont courants — les ré-attester serait du churn (#5, déjà signalé ce jour,
cf. 121234/134254). J'ai donc visé une **classe de vérification que la mémoire ne couvre PAS** :
la **compatibilité Python future** du corpus de tests.

**Hypothèse de risque.** Le runner tourne aujourd'hui sous **Python 3.10.12** ; toutes les suites
sont VERTES. Mais un `DeprecationWarning`/`SyntaxWarning`/`ResourceWarning` émis silencieusement
aujourd'hui (ex. escape-séquences d'expression régulière non-`r""`, `datetime.utcnow()`, sockets/
fichiers non fermés) reste **invisible aux tables de currency** : la CI est verte tant que le warning
n'est pas promu en erreur — ce qui arrive tout seul à un futur bump d'interpréteur. Un vrai **faux
vert forward-looking**, orthogonal aux axes *teeth* (qui prouvent que les gates mordent *aujourd'hui*).

**Méthode (lecture seule).** Rejeu des **25 suites** de `gate.needs` (bijection 25 dossiers `tests/`
↔ 25 jobs, déjà gatée) avec les warnings promus en erreurs :
`python3 -W error::DeprecationWarning -W error::SyntaxWarning -W error::PendingDeprecationWarning -W error::ResourceWarning -m unittest discover -s tests`
depuis le `working-directory` de chaque module. Le `tests/` **racine** a aussi été balayé : il rend
« Ran 0 tests » — attendu, c'est la suite **Playwright e2e** (`smoke.spec.ts`, TypeScript, job manuel
`workflow_dispatch` `e2e-baseline` hors gate de merge car exige un serveur live), pas une suite python.

**Résultat — CLEAN.** **25/25 suites OK** sous warnings-as-errors (624 tests au total, cohérent avec
`regression_run.json`). **0 warning latent** dans tout le corpus python : le corpus est prêt à un
futur durcissement d'interpréteur sans régression silencieuse. Aucune suite ne dépend d'une API
dépréciée ni n'émet de `ResourceWarning` (pas de handle de fichier/socket fuité dans les fixtures).

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit** (aucun
`out/*.json` touché → pas de rebuild `quality_report`, cf. [[audit4big-rebuild-after-doc-edits]]),
**0 gate ajouté** (#5 — attester ≠ multiplier les gates ; ce n'est pas une *classe de dérive* mais
une propriété forward-compat, re-jouable à la demande via la commande ci-dessus après tout bump
Python ou ajout de suite), **0 chiffre saisi à la main** (#6 — 25 et 624 dérivés de `gate.needs`/du
runner), **0 commande VPS** (#8 — sweep 100 % local & lecture seule). Seule édition : ce journal +
une entrée mémoire neuve.

**Vérif.** `git status --short` avant le sweep = vide (lecture seule) ; `./run_ci.sh` après →
**33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché). Nouvelle mémoire *forward-compat warnings
sweep* consignée pour re-jeu futur.

---

## Session `20260805_144304` — Currency canal stakeholder + consignation d'un incident d'intégrité

**Constat.** `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** ; roadmap fonctionnelle
intégralement livrée+gatée, restes hors périmètre worker (VPS/EAS/ONAPI · #8). Le canal
stakeholder `daily_reports/2026-08-05.md` s'arrêtait à la session `054200`. **Sept commits**
ont suivi dans la journée. La ré-attestation de `9b6ec60` (`121234`) avait confirmé le canal
courant MAIS n'éditait que ce journal, **pas le fichier daily_report** — qui laissait donc
croire à une journée VERTE ininterrompue alors qu'un **tree commité RED** est survenu depuis.

**Gap réel fermé.** `127ef6f` (`124241`) annonçait « 33 PASS » mais commitait un tree
**RED (32 PASS · 1 FAIL)** — le gate `guard_constraints`/`constraints-guard` mordant sur le
tree lui-même ; `1321983` (`131244`) a rattrapé la régression sous ~50 min (tree revenu VERT).
Cet **incident d'intégrité matériel pour le stakeholder** était absent du canal daily_report.
Ajouté : section fin-de-journée `144304` documentant (a) l'incident RED→VERT (preuve que la
discipline de gate est opposable au tree commité, pas seulement en pré-commit), (b) les deux
sweeps lecture-seule qui ont suivi (docstring-vs-code `48b19b9` CLEAN 26 modules · forward-compat
warnings `f311a1b` CLEAN 25/25), (c) ré-attestation du point courant HEAD `f311a1b`.

**Recompute indépendant (aucun chiffre saisi · #6).** `quality_report.json` = 24 modules ·
tous == 100 · `coverage.ok=true` · verdict PASS · `regression_run.json.totals` = suites 24 ·
ran 624 · passed 624 · 0/0/0 · `run_ci.sh` = 33 PASS = 8 statiques + 25 suites. Toutes les
tables du daily_report **concordent** ce jour.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit** (aucun
`out/*.json` touché → pas de rebuild `quality_report`, cf. [[audit4big-rebuild-after-doc-edits]]),
**0 gate ajouté** (#5 — l'intégrité CI est déjà gatée par `check_ci_integrity` + `guard_constraints`),
**0 chiffre saisi à la main** (#6), **aucune commande VPS** (#8 — 100 % local). Gates re-joués
sur l'arbre édité : `check_docs` ✅ (liens internes OK), `guard_constraints` ✅. Éditions :
daily_report `2026-08-05.md` (section `144304`) + ce journal. Classe [[two-logging-channels]].

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché).

---

## Session `20260805_171321` · Sweep docstring-vs-code étendu aux helpers `*lib/` — CLEAN, 1 faux-positif neuf réfuté

**Constat.** `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. Roadmap fonctionnelle
intégralement livrée+gatée ; restes hors périmètre worker (VPS/EAS/ONAPI · #8). Aucune
tâche fonctionnelle in-repo résiduelle. Valeur honnête du jour : étendre la veille
d'intégrité `docstring-vs-code` (classe RÉCURRENTE/non-close, cf. [[docstring-vs-code-drift]])
à la **surface qu'un balayage scopé `*_gen.py` rate** : les paquets d'aide `*lib/`.

**Sweep exécuté (lecture-seule).** Fan-out `Explore` scopé UNIQUEMENT aux helpers
`seolib` · `publiciste/lib` · `deploylib` · `dvlib` · `finlib` · `wflib` · `commlib` ·
`ecflib` · `genlib` — docstrings module + fonction, directions A (annonce non-implémentée)
ET B (omet une sortie réelle) → **0 dérive réelle**.

**Faux-positif neuf réfuté (vérifié en source + contre le spec).** Le seul candidat
remonté : `crm/financement_bancaire/finlib/gate.py:58`, docstring de `apport_requis_usd`
« Apport requis = prix × taux (**20 % résident RD, 30 % étranger**) », soupçonnée de
« coder en dur une constante que le code lit du spec ». **NON — accurate-to-contract** :
la parenthèse **illustre les valeurs COURANTES du spec**, vérifiées égales à
`financement_spec.json` (`resident_rd`=20 · `etranger`=30) ; elle reflète la ligne 8 de la
MÊME docstring module (jugée correcte) ; les lignes 15-18 posent déjà le cadre « JAMAIS
codés en dur, viennent du contrat ». Le code (l.59-64) lit bien `cfg.taux_apport_pct[...]`.
→ « docstring citant la valeur courante d'un nombre spec-driven en illustration » =
même famille que l'exclusion pédagogique/illustrative. **LAISSÉ** (anti-churn #5) — pas
d'harmonisation, pas de re-flag. Leçon consignée en mémoire : vérifier la valeur COURANTE
du spec avant de qualifier de contradiction-hardcode.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**
(aucun `out/*.json` touché → pas de rebuild `quality_report`, cf.
[[audit4big-rebuild-after-doc-edits]]), **0 gate ajouté** (#5 — les docstrings sont de la
prose, faible valeur à gater ; la parenthèse est de surcroît exacte), **0 chiffre saisi à
la main** (#6 — les 20/30 sont lus du spec, non retapés), **aucune commande VPS** (#8 —
100 % local). Éditions : mémoire `docstring-vs-code-drift.md` (note du faux-positif) + ce
journal.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (aucun fichier gaté touché).

---

## Session 20260805_174324 · GATE NEUF — prose #9 des modules bancable + commissions ancrée sur CLAUDE.md

**Contexte.** Repo vert au démarrage (33 PASS · 0 FAIL · 0 SKIP), tous modules livrés,
roadmap 8-sem sans tâche-feature ouverte. Sonde ciblée sur la classe **claude-md-constant-anchor-gate** :
les constantes canoniques #9 (3 % édition · 8.5 % marketing · 52 % point d'équilibre)
recopiées hors du module generator.

**Constat (mutation-prouvé).** `sed` #9 `8.5 %→9 %` sur `CLAUDE.md` → le gate
`check_readme_claims.sh` fait rougir **8 surfaces generator/template** (model.py CANONICAL,
README, renderer `_CANONICAL_MARKERS`, oracle test, 2 docstrings scorer, template intro/§5.2/§7)
— l'ancre est vivante. **MAIS 4 copies restaient périmées EN SILENCE**, dans DEUX modules
DISTINCTS, chacune se réclamant explicitement « (#9) » / « CLAUDE.md #9/#10 » :
- (a) `faisabilite/bancable/README.md` — « 3 % édition » et « 8.5 % marketing » (#9)
- (b) `faisabilite/bancable/banclib/finance.py` — MÊME phrase, en docstring
- (c) `faisabilite/bancable/banclib/deps.py` — « CLAUDE.md #9/#10 (3 %/8.5 %/52 % · USD+DOP · Cardnet · Letter US) » (6 marqueurs)
- (d) `crm/commissions/README.md` — « pourcentages canoniques (3 % édition · 8.5 % marketing · 52 % point d'équilibre) »

Corriger le generator ne TOUCHE pas ces modules → invention #6 latente, de la classe déjà
fermée 10× (publiciste #10 · fiscal moneda · fiches #10 · template canonique).

**Fix.** Extension du bloc d'ancrage existant dans `ci/check_readme_claims.sh` (juste après
« Template canonique »), **réutilisant `exp`** déjà recomputé des lignes #9/#10 de `CLAUDE.md`
(zéro re-parse, zéro duplication). Un claim absent échoue AUSSI (traçabilité #6). Documenté
en `ci/README.md` §2 (paragraphe « paramètres canoniques »), pas de nouvelle ligne au
tableau §1 (cf. [[ci-readme-table-detail-in-section2]]).

**Teeth (mutation-vérifié, arbre restauré à chaque fois).**
- `#9 8.5 %→9 %` → mord les **4** surfaces.
- `#9 52 %→55 %` → mord **(c)+(d) SEULEMENT** ; (a)/(b) ne citent qu'édition+marketing ⇒
  correctement épargnées = **ciblage précis** (pas de faux-rouge).
- `#10 Cardnet→Azul` → mord l'énumération 6-marqueurs de (c).
- Arbre restauré → gate exit=0, les 4 surfaces re-vertes.

**Portée / anti-churn.** **0 fichier de production édité** (seuls `ci/check_readme_claims.sh`
+ `ci/README.md`), **0 artefact reconstruit** (aucun `out/*.json` ni module doc scoré par
audit_4big → pas de rebuild `quality_report`), **0 chiffre saisi à la main** (#6 — les 6
valeurs sont recomputées de `CLAUDE.md`, jamais retapées), **aucune commande VPS** (#8 —
100 % local). Classe RÉCURRENTE (chaque module distinct qui recopie une constante #9/#10
citée est une surface à ancrer) : nouvelle sous-surface (bancable + commissions) close.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.

---

## Session 20260805_194334 · GATE NEUF — complétude de l'énumération des gates statiques dans la fiche QA

**Contexte.** Repo vert au démarrage (33 PASS · 0 FAIL · 0 SKIP), tous modules
livrés, roadmap 8-sem sans tâche-feature ouverte. Sonde ciblée sur la classe
**fiche-completeness** (cf. [[fiche-accuracy-audit-closed]] — « completeness≠accuracy,
diff deliverables vs fiche tables »), cette fois sur une fiche qui **énumère** un
ensemble censé être exhaustif.

**Constat (drift présent-temps réel).** La fiche `03_agents/qa/AGENT.md`
(§« Deuxième étage QA · batterie de gates statiques `ci/*.sh` ») liste les gates dans
un tableau, en se **déclarant explicitement** (l.49) « la liste ci-dessous est donc
*l'état courant*, pas une constante ». Or le tableau ne comptait que **7 lignes** alors
que `git ls-files 'ci/*.sh'` (hors `lib.sh`) en rend **8** : le 8ᵉ gate
`check_mobile_workflow.sh` — livré **le jour même** (session `001109`, cf. rapport
matinal 33-jobs) — n'y figurait **NULLE PART** (`grep -c check_mobile_workflow` fiche
= **0**, les 7 autres ≥ 1). La fiche promettait la complétude sans la tenir : omission
silencieuse d'un jour entier. **Surface ungatée confirmée** : `check_ci_integrity`
verrouille le **câblage CI** (`ci/*.sh` ↔ job `gate.needs`) mais **ne lit pas la fiche** ;
`check_readme_claims` ne recomputait aucune énumération de gates → aucun gardien.

**Fix (2 volets).**
1. **Correction du fond** : ajout de la ligne manquante `check_mobile_workflow.sh` au
   tableau, placée juste après `check_ci_integrity.sh` (les deux gardent l'intégrité
   d'un fichier-workflow) — rôle décrit fidèlement d'après l'en-tête du gate (second
   workflow `mobile-build.yml`, hors `gate.needs`, gating `if:` job-level sur
   `needs.preflight.outputs.*` jamais `secrets.`, contrat d'outputs, MOB-1..4).
2. **Ancrage anti-récurrence** : extension du gate existant `ci/check_readme_claims.sh`
   (bloc Python ajouté avant `sys.exit`, réutilise `subprocess`/`bad`/`good` déjà en
   scope · zéro nouveau gate #5 · zéro nouveau fichier) — re-dérive l'ensemble depuis
   `git ls-files 'ci/*.sh'` (hors `lib.sh`, #6) et exige que chaque basename soit cité
   (backtické) dans la fiche ; un gate **absent** échoue. Rend la promesse « l'état
   courant » **opposable**.

**Teeth (mutation-vérifié, arbre restauré à chaque fois).**
- **MUT1** — retrait de la ligne `check_mobile_workflow.sh` → **RED** nommant le gate
  réel non cité (`n'énumère PAS ['check_mobile_workflow.sh'] … 8 gates réels`).
- **MUT2 (forward-protection)** — `git add` d'un 9ᵉ `ci/check_dummy9.sh` factice (index
  seul) → **RED** (`… ['check_dummy9.sh'] … 9 gates réels`) : le **prochain** gate
  livré ne pourra pas être omis en silence. Fichier `--cached` retiré + `rm` (arbre
  byte-restauré, **JAMAIS** `git clean` · CLAUDE.md).
- Arbre restauré → gate exit 0, les 8 gates re-verts.

**Portée / anti-churn.** **0 fichier de production édité** (seuls la fiche QA,
`ci/check_readme_claims.sh`, `ci/README.md`), **0 artefact reconstruit** (aucun
`out/*.json` ni doc scoré par audit_4big → pas de rebuild `quality_report`, cf.
[[audit4big-rebuild-after-doc-edits]]), **0 chiffre saisi à la main** (#6 — le « 8 » est
recompté de `git ls-files`, jamais tapé), **aucune commande VPS** (#8 — 100 % local).
Documenté en `ci/README.md` §2 sous `check_readme_claims.sh` (nouveau paragraphe
« COMPLÉTUDE de l'énumération des gates statiques »), pas de nouvelle ligne au tableau §1
(cf. [[ci-readme-table-detail-in-section2]]).

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.

---

## Session `20260805_201339` · ATTESTATION chaîne d'intégrité des constantes mandataires — bout-en-bout · 0 édition de production

**Contexte.** Roadmap intégralement livrée+gatée ; baseline `run_ci.sh` verte (**33 PASS**).
Aucune tâche fonctionnelle in-repo ne reste (cf. sessions du jour). Plutôt qu'ajouter
un gate marginal (risque #5, cf. [[verify-uncovered-before-gating]]), la valeur honnête
du jour : **prouver de bout en bout que la chaîne de dérivation des constantes du mandat
n'a AUCUN maillon hardcodé ni maillon ungaté**, la classe de bug latent la plus coûteuse
(un générateur qui fige `0.52`/`8.5 %`/`#f0b429` en littéral dérive en silence si CLAUDE.md
change — précédent réel `bancable_gen.py 0.52→finance._pct`, cf.
[[fix-vs-gate-transitively-protected-constant]] · [[claude-md-constant-anchor-gate]]).

**Sonde 1 — aucun littéral hardcodé côté générateurs.** Fan-out Explore sur
`05_deliverables_mvp/**` (recherche `0.52`/`0.085`/`0.03`/`3 %`/`8.5 %`/`52 %`/`#0a0a12`/
`#f0b429`/`Cardnet`/`USD`/`DOP`/`Fraunces`/`Cormorant` en CODE exécutable des générateurs) :
**0 littéral non-sourcé**. Tous les générateurs lisent depuis **2 sources canoniques uniques** :
- `faisabilite/generator/genlib/model.py` → dict `CANONICAL` (6 valeurs : édition 3 %,
  marketing 8.5 %, équilibre 52 %, devises USD+DOP, Cardnet, Letter US) — consommé par
  bancable (`finance._pct(deps.CANONICAL[...])`), report, renderer.
- `publiciste/lib/branding.py` → `COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`
  (#0a0a12 · #f0b429 · Fraunces · Cormorant Garamond) — importé par mobile/app_config,
  frontend/chat_otoia, seo (via `deps.branding`), publiciste/generator.

**Sonde 2 — les 2 sources canoniques sont elles-mêmes ancrées à CLAUDE.md.** Le maillon
critique : une source unique n'est correcte que si ELLE est gatée contre le mandat. Vérifié
dans `ci/check_readme_claims.sh` :
- bloc l.5427 — `model.py CANONICAL` **re-dérivé de CLAUDE.md #9/#10** (6 valeurs).
- bloc l.6958 — `branding.py` CONSTANTES **re-dérivées de CLAUDE.md #4** (2 couleurs +
  2 fontes) + docstring + README:43 + oracle du test publiciste (+ `renderer._CANONICAL_MARKERS`).
→ boucle fermée : générateur → source canonique → CLAUDE.md, **sans trou**.

**Sonde 3 — les 2 ancres-pivots MORDENT (mutation-prouvé, arbre byte-restauré).**
- MUT1 `branding.py COLOR_ACCENT #f0b429→#e0a420` → **RED** : « Branding canoniques ·
  branding.py CONSTANTES DÉRIVENT de CLAUDE.md #4 : accent='#e0a420' (attendu '#f0b429') ».
- MUT2 `model.py CANONICAL marketing_pct 8.5 %→9 %` → **RED** : « Faisabilité canoniques ·
  model.py CANONICAL DÉRIVE de CLAUDE.md #9/#10 : marketing_pct='9 %' (attendu 8.5%) ».
- Restauration ciblée par `git checkout -- <path>` (**JAMAIS `git clean`**, #interdits) →
  `git status --porcelain` **vide** · gate re-vert.

**Faux-positif écarté (settled, NON édité).** Le tableau « launch-readiness / Attestation »
en tête du daily report `2026-08-05.md` (l.30/45) lit « 624 passés · 0 skip » alors que
`regression_run.json.totals` = 607 passés / 17 skip. Ce n'est **pas une dérive neuve** :
la §CURRENCY du même fichier (l.146-171) reconcile explicitement (« les 624 passés · 0 skip
des sections antérieures sont des snapshots datés », nouvelle baseline 607/17 posée par
`1b99917`) — classe *dated-snapshot = KEEP* déjà consignée [[regression-baseline-17-skips-by-design]].
Re-l'éditer = re-litiguer un settled → LAISSÉ.

**Périmètre.** **0 édition de production**, **0 gate ajouté** (#5 — chaîne déjà défendue
end-to-end, sonde = attestation pas nouvelle machinerie), **0 artefact reconstruit**,
**0 chiffre saisi à la main** (#6), **aucune commande VPS** (#8 — 100 % local). Classe
mandate-constant-integrity **CLOSE CLEAN** ce jour (générateurs 0 hardcode · 2 sources ·
2 ancres CLAUDE.md prouvées mordantes).

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre.

---

## Session `20260805_204339` · CURRENCY canal stakeholder porté au HEAD + balayage lecture-seule multi-surface (0 édition de production)

**Constat.** Le rapport stakeholder `05_deliverables_mvp/daily_reports/2026-08-05.md`
attestait à HEAD **`1a44dfe`** (session `181325`, 18:13) et **ne reflétait pas** deux jalons
plus tardifs du même jour : `afa6165` (`194334`, gate neuf `fiche-enumeration-completeness`)
et `27c7d20` (`201339`, attestation chaîne d'intégrité des constantes). Le canal roadmap
(distinct du journal `05_activity_log/` per la discipline « deux canaux ») avait donc lapsé
de 2 milestones. Précédent identique : `1b99917` (« CURRENCY canal stakeholder »).

**Vérification amont (avant écriture, tout recomputé d'artefacts committés · #6).**
- Docstring↔code des 25 générateurs : sorties + comportement **CLEAN** (sous-classe
  filename déjà close 22/22 ; aucune nouvelle dérive input/fallback).
- Comptes de tests par fiche AGENT.md vs `def test_` réels : **concordants** — CRM `81`
  (trio 25+31+25) + financement `35` · ecf `39` · confotur `44` · seo `36` · portails `19`
  · chat `31`. Tous déjà gatés.
- Constantes CLAUDE.md #9/#10 (`3 %`/`8.5 %`/`52 %`/USD+DOP/Cardnet/Letter) :
  `check_readme_claims.sh:5355+` les **re-dérive** de `CLAUDE.md` → `genlib/model.py` +
  renderer markers + README ancrés. Aucune copie orpheline.
- Runtime mobile : Expo **51 actuel / 54 cible** cohérent sur toutes les surfaces « en
  place » (AGENT.md, `AGENTS_EXISTING_ASSETS §8`, roadmap L18/L56).
- `regression_run.json` : **624 ran = 650 méthodes − 26** (la suite `qa-regression`
  s'exclut de son propre méta-run) — self-consistant, byte-gaté par `check_artifacts`.
- Fiche QA : cite désormais les **8/8** gates (`git ls-files 'ci/*.sh'` hors `lib.sh` = 8),
  verrouillé par le gate `fiche-enumeration-completeness` (`afa6165`).

**Édition.** Section « Mise à jour de currency · fin de journée (HEAD `27c7d20`) » ajoutée
au rapport stakeholder : tableau des 2 jalons (nature + opposabilité au merge) + état de
vérification indépendant + portée anti-churn. **Structure du gate inchangée** : `run_ci.sh`
→ **33 PASS** = **8 gates statiques + 25 suites** (aucun jalon n'ajoute de job).

**Portée / anti-invention.** **0 fichier de production édité**, **0 artefact reconstruit**
(le rapport n'est pas scoré par `audit_4big` — seuls les docs modules le sont), **0 gate
ajouté** (#5 — la journée était déjà entièrement gatée), **0 chiffre saisi à la main** (#6 —
`8`/`33`/`624` recomputés de `git ls-files`/`run_ci.sh`/l'artefact), **aucune commande VPS**
(#8). Seules éditions : le rapport de currency + ce journal.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.

---

## Session 20260805_211340 · GATE — complétude de l'énumération des gates dans la **table §1 de `ci/README.md`** (jumeau ungaté de la fiche QA, fermé `afa6165`)

**Contexte + choix de tâche.** Arbre propre au démarrage, `./run_ci.sh` → **33 PASS · 0
FAIL · 0 SKIP** ; roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée in-repo.
Hypothèse dirigée par la mémoire *prose-facts-vs-numeric-drift* (5ᵉ sous-classe) : le 8ᵉ
gate statique `check_mobile_workflow.sh` livré ce jour a pu laisser des **siblings périmés**
dans les gate-docs ungatés. Sweep repo-wide `\b7 (gates?|statiques?)\b` / `\b32 (jobs?|PASS)\b` :
**0 dérive réelle** — toutes les occurrences « 7 »/« 32 » sont des **snapshots datés
historiques** (daily_reports/activity_log 08-02→08-05) ou **pédagogiques** (le commentaire
`check_mobile_workflow.sh:13` « 7/7 gates statiques restaient verts » désigne les 7 gates
**pré-existants** qui motivaient un 8ᵉ), classe correctement `historical-repro`/`pedagogical`
= KEEP. Docs present-tense (ci/README §1, `8ᵉ gate statique`) toutes cohérentes à 8/33.

**Le finding réel — une surface UNGATÉE.** La **table §1 de `ci/README.md`** (« Ce que fait
le pipeline ») est LA doc canonique des jobs statiques : une ligne par gate `ci/*.sh`. Or sa
**complétude** était le jumeau exact de la fiche QA fermée par `afa6165` — mais **non gaté** :
`check_ci_integrity` verrouille le CÂBLAGE (`ci/*.sh` ↔ `gate.needs`), le bloc fiche-QA (ajouté
`afa6165`) ne lit que `03_agents/qa/AGENT.md`. **Rien** n'exigeait que chaque gate figure dans
la table §1 → un 9ᵉ gate y serait omis EN SILENCE (même mode de défaillance que la fiche QA
avait subi un jour entier). Prouvé ungate **par mutation** (verify-uncovered) : retrait de la
**ligne de table** `check_mobile_workflow` → `run_ci --static` **8 PASS** (aucun gate ne mord).

**Fix — extension du gate existant (PAS un nouveau job · #5).** Bloc Python ajouté à
`ci/check_readme_claims.sh` (avant `sys.exit`), **réutilisant** `_gate_names` déjà re-dérivé de
`git ls-files 'ci/*.sh'` (hors `lib.sh` · #6 — jamais une liste à la main). Il exige que chaque
basename soit cité dans une **ligne de TABLE** (`|`…) de la **section §1** — délibérément PAS la
prose §1 (qui ne mentionne que certains gates pour l'explication `mobile-build.yml` : un contrôle
whole-section serait **édenté** sur un retrait de ligne de table, comme la mutation l'a révélé —
d'où le scope table-rows). 0 nouveau gate `ci/*.sh`, 33 jobs inchangés.

**Teeth mutation-prouvés (2 axes).** MUT1 — suppression de la vraie ligne de table
`check_mobile_workflow` (l.24) → **RED** nommant `check_mobile_workflow.sh` (exit 1). MUT2 —
9ᵉ `ci/*.sh` factice tracké (index seul) → **RED** sur **les DEUX** surfaces (fiche QA + table §1 :
forward-protection, le prochain gate ne pourra être omis d'aucune) ; `git rm --cached` + `rm`,
**arbre byte-restauré** (JAMAIS `git clean`). *Piège écarté* : un premier essai `sed 15d` visait
la ligne 15 = **en-tête** de table (le `grep -n` renumérotait relatif au pipe) — mutation inerte,
détectée et re-ciblée sur la vraie ligne 24.

**Doc.** Sous-section « COMPLÉTUDE de l'énumération dans la TABLE §1 » ajoutée en **§2** de
`ci/README.md` (discipline *ci-readme-table-detail-in-section2* : ne pas grossir la cellule §1).

**Périmètre.** **0 édition de production** (générateur/out/), **0 artefact rebuild** (aucune doc
scorée `audit_4big`), **0 chiffre saisi à la main** (#6 — `8`/`9` recomputés de `git ls-files`),
**aucune commande VPS** (#8). Seules éditions : `ci/check_readme_claims.sh` (+40 l) et
`ci/README.md` (+13 l).

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.

---

## Session `20260805_214344` · CURRENCY canal stakeholder porté au HEAD `cb3de78` + audit lecture-seule (0 édition de production)

**Contexte.** Repo VERT à l'ouverture (`./run_ci.sh` → 33 PASS · 0 FAIL · 0 SKIP). Codebase
très convergé, lourdement gaté. Balayage préalable pour un finding **réel** (pas un gate N+1
redondant · #5) avant de retomber sur la maintenance de currency.

**Audit lecture-seule (0 dérive trouvée).**
- **docstring↔code des 25 générateurs** (sous-agent Explore, classe *docstring-vs-code-drift*
  marquée RÉCURRENTE) : chaque docstring/README annonce **exactement** les fichiers `out/`
  écrits par le code ; complétude des fiches OK (21 sous-modules ↔ ≥1 `03_agents/*/AGENT.md`).
  **CLEAN.**
- **SEO roadmap L60** (Sprint 6 · trilingue FR/EN/ES · schema.org · hreflang) déjà **livré** :
  `out/seo_keywords.json` = **258** (fr=87 · en=87 · es=84, recomptés de l'artefact) +
  `seo_schema_org.json` + `seo_hreflang.json` ; le triplet est **byte-gaté** par
  `check_readme_claims.sh` (`keywords_total`+`keywords_per_lang`). **Pas un gap.**

**Le finding réel — canal stakeholder stale d'un jalon.** Le rapport roadmap
`05_deliverables_mvp/daily_reports/2026-08-05.md` attestait au HEAD **`27c7d20`** (session
`201339`) mais **`cb3de78`** (session `211340`, gate `fiche-enumeration-completeness` 2ᵉ locus
= table §1 de `ci/README.md`) avait suivi et n'y était pas reflété. Discipline *two-logging-channels*
(canal roadmap distinct du présent journal per-session) : porté au **HEAD courant**.

**Édition.** Section « Mise à jour de currency · fin de journée (HEAD `cb3de78`) » ajoutée au
rapport : tableau du jalon `cb3de78` (nature + opposabilité au merge) + état de vérification
indépendant (complétude des gates verrouillée sur **les deux** loci — fiche QA `afa6165` + table
§1 `cb3de78` — par le même `check_readme_claims.sh`) + portée. Structure du gate **inchangée** :
8 gates statiques (`git ls-files 'ci/*.sh'` hors `lib.sh` = 8, recompté) + 25 suites ; `cb3de78`
ne touche que l'infra CI (`ci/README.md`, `ci/check_readme_claims.sh`) + journal, aucun job.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate
ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8`/`33`/`258` recomputés de `git ls-files` ·
`run_ci.sh` · l'artefact), **aucune commande VPS** (#8). Seules éditions : le rapport de currency
+ ce journal.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.

---

## Session `20260805_221354` · SWEEP de vérification — bijection INVERSE des artefacts `out/` (aucun fichier commité orphelin) · 0 édition de production

**Contexte.** Repo VERT à l'ouverture (`./run_ci.sh` → 33 PASS · 0 FAIL · 0 SKIP), très
convergé. Balayage lecture-seule multi-surface préalable pour un finding **réel** avant tout
travail (discipline anti-gate-N+1 · #5) :
- **CI** : 8 gates statiques (`git ls-files 'ci/*.sh'` hors `lib.sh` = 8) + 25 suites = 33 jobs.
- **Complétude modules** : **24/24** modules livrés (22 `*/*/tests` + `seo/tests` + `publiciste/tests`)
  CI-câblés (`gate.needs`), régression-couverts (`regression_plan.json` = 24 suites, `publiciste`
  inclus), fiche-documentés (≥1 `03_agents/*/AGENT.md`). **Aucun orphelin.**
- **Dérive doc↔code** (sous-agent Explore, 3 classes : docstring-vs-code · prose-vs-numeric ·
  fiche-completeness) → **CLEAN**, 0 dérive présente-temps.
- **Registre décisions ouvertes** (`OPEN_DECISIONS_REGISTER.md`) : les **5** items (D-01..D-05)
  re-vérifiés **encore ouverts**, chaque citation `file:line` **encore exacte** (D-01 `pie_spec.json:24`
  `contrats→legal/confotur` + 5 downstreams `null` · D-02 `gate.py:142` `_cond_validation_wag` +
  `financement_spec.json:157` · D-03 `chat.py` absent fs / `CLAUDE.md:28` le liste / footer L128
  l'omet · D-04 `projets_editor.py` absent fs · D-05 bundle `a_confirmer`/`null`). **Contenu à jour**
  (tampon de session en retard, mais bumper sans changement de contenu = churn cosmétique — écarté).
- **publiciste** : `build/` **`.gitignore`-d** (artefacts reproductibles non commités · pattern
  `bancable-out-not-committed`), pas de sous-commande `build` ni de `out/` commité → `check_artifacts`
  le saute correctement ; fiche L105 « aucun `index.html` commité » **exacte**. **Pas un gap.**
- **Sweep CWD-hétérogène écarté** : `check_artifacts` (l.62) exécute chaque générateur **depuis son
  propre répertoire module** ; un sweep « CWD différent » produirait des **faux positifs** (les
  générateurs résolvent leurs entrées relativement au module par conception) — piège de faux-positif
  signalé à répétition en mémoire, abandonné.

**Le finding — axe de robustesse génuinement NON couvert.** `check_artifacts.sh` prouve la
direction **AVANT** : pour chaque fichier **produit** par `build` (`$tmp/*`), un jumeau commité
**identique** existe (l.71-93 : itère sur les fichiers produits). Il ne vérifie **jamais l'INVERSE** :
qu'un fichier `out/` **commité** est **toujours** produit par un `build` frais. Un fichier `out/`
**orphelin** — émis par un ancien générateur, plus produit — passerait **silencieusement** (aucune
boucle sur les fichiers commités). Sibling non-déterminisme des sweeps `hash-seed` / `locale-TZ` /
`forward-compat` déjà en mémoire, mais sur une propriété **différente** (complétude/orphelins des
artefacts commités, pas déterminisme).

**Sweep exécuté (lecture seule, rejouable).** Pour chaque `out/` (21 répertoires, tous avec un
générateur `build`), `build` frais vers un `mktemp -d`, puis pour chaque fichier `git ls-files`
du `out/` commité : exiger sa présence dans le build frais. Résultat :
- **58** fichiers `out/` commités inspectés · **57 reproduits** par `build` (bijection inverse OK).
- **1 seul « orphelin »** : `qa/regression/out/regression_run.json` — **BY DESIGN**, artefact
  d'**EXÉCUTION** (`run`, pas `build`) explicitement exclu du contrat `build` par la docstring de
  `check_artifacts` (l.21-24) et **byte-gaté séparément** par `check_regression.sh` (régénère un
  `run` frais, exige l'identité byte-for-byte · mémoire `regression-baseline-17-skips-by-design`).
- → **0 orphelin réel.** La bijection inverse **tient** ; confiance durable ajoutée.

**Pourquoi PAS un gate (#5).** La direction AVANT est déjà gatée (`check_artifacts`) ; l'INVERSE
est empiriquement **propre** et un orphelin réel serait de toute façon **transitivement attrapé**
(tout consommateur à build reproductible lisant un `out/` périmé casserait sur `check_artifacts`).
Ajouter un gate N+1 pour une propriété tenue = machinerie redondante. Consigné comme **sweep
rejouable**, comme ses siblings déterministes — pas un `ci/*.sh`.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate
ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8`/`24`/`33`/`58`/`57` recomputés en direct),
**aucune commande VPS** (#8). Seule édition : ce journal (+ mémoire projet).

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.

---

## Session `20260805_231404` — CURRENCY canal stakeholder porté au HEAD + sweep docstring↔code CLEAN

**Contexte.** HEAD = `8903a4b`. Le rapport roadmap stakeholder (`daily_reports/2026-08-05.md`,
discipline « deux canaux ») s'arrêtait au jalon `cb3de78` (session `211340`) et ne reflétait
pas **2** jalons plus tardifs du même jour.

**Verif primaire — chasse aux défauts (classe docstring↔code, marquée RECURRING/non-fermée).**
Audit fan-out des docstrings de production vs comportement réel du code sur tous les `*_gen.py`
+ helpers `*lib/` (parser/model/report/deps/builder/validator) : sorties écrites, flags argparse,
comportements (anti-invention, déterminisme, invariants). **Résultat CLEAN** — aucune dérive
haute-confiance. Non-défauts connus ré-écartés : `{{placeholder}}` (constraint #6), compat
`validate [-o OUT]` self-consistant, chemins backtick deliverables-relatifs.

**Verif secondaire — nature des 2 jalons portés :**
- `d6a8d73` (`221354`) — SWEEP bijection INVERSE `out/` (commité ⊆ produit) : **0 orphelin** ;
  read-only, seul le journal touché. NON-gate (#5).
- `8903a4b` (`224359`) — FIX comment-vs-code dans `ci/validate_json.sh` : le commentaire
  évoquait un contrôle `$schema`/draft que le code ne fait pas (parse-only) ; aligné + renvoi
  à l'oracle `jsonschema` (17 skips par design). **0 logique de gate altérée.**

**Recompte structure de merge (aucun chiffre saisi · #6) :** `git ls-files 'ci/*.sh'` hors
`lib.sh` = **8** gates statiques (inchangé — aucun jalon n'ajoute de job) ; `run_ci.sh` =
**33** = 8 + 25 suites.

**Édition.** Section « Mise à jour de currency · fin de journée (HEAD `8903a4b`) » ajoutée au
rapport : tableau des 2 jalons (nature + opposabilité au merge) + état de vérification indépendant
+ portée. Structure du gate **INCHANGÉE**.

**Portée / anti-churn (session `231404`).** **0 fichier de production édité**, **0 artefact
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6), **aucune commande
VPS** (#8). Seule édition : ce journal + le rapport de currency.

**Vérif.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** · `git status` propre après commit.
