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

## Session `20260806_214700` — VÉRIF · axe NEUF « cohérence chronologique & validité calendaire de la PISTE D'AUDIT (activity_log + daily_reports) » → CLEAN 214 stamps · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP** (gate complet), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel. Objectif : un **axe teeth encore jamais balayé**. Trois candidats écartés en amont car **déjà couverts** (vérifié dans la mémoire + le journal du jour) : la portabilité des NOMS de fichiers (session `184624`), l'intégrité des ancres-fragment Markdown `](#slug)` (session `174619` — mon détecteur a d'ailleurs reproduit son unique « pendante » = la citation-prose du lien README dans ce log, faux-positif correctement écarté par la session d'origine), et la validation flat-`out/` ↔ schéma (by-design : les oracles jsonschema valident le bundle **en mémoire**, pas le fichier plat — cf. `schema-oracle-ref-integrity`).

**Axe NEUF (absent de la mémoire).** TOUS les balayages précédents portent sur le **contenu** des artefacts/sources (comptes, slugs, schémas, encodage octet/NFC, JSON-strict, portabilité des chemins) ou la **structure** de l'arbre. Jamais éprouvée : l'**intégrité de la piste d'audit elle-même** — les **horodatages** que le worker inscrit dans `05_activity_log/*.md` et `daily_reports/*.md`. C'est le socle de la contrainte **#6 (zéro invention)** appliquée à **notre propre journal** : un futur agent qui inscrirait un `## Session 20260807_…` (futur), ou `20260805_…` dans le fichier `2026-08-06.md` (date ≠ nom-de-fichier), ou une date-calendrier impossible (`20260231`), ou une heure `25:00:00`, **garderait toute la CI verte** (aucun gate ne regarde les horodatages) tout en **corrompant silencieusement la traçabilité** — un faux-vert d'**honnêteté**, invisible à toute la chaîne. Distinct de `two-logging-channels` (qui vérifie que chaque *chiffre* d'un daily_report est sourcé d'un artefact commité, jamais les *dates* du journal).

**Détecteur (stdlib pur `datetime` + `git ls-files`, rejouable, read-only).** Sur **37 fichiers** (8 `activity_log` + 29 `daily_reports`) et **214 marqueurs `## Session AAAAMMJJ_HHMMSS`** (headers uniquement — les citations-prose d'autres sessions sont exclues du critère de monotonie) :
- **Validité calendaire** : `datetime.date(y,m,d)` construit sans `ValueError` ; heure `HH<24 · MM<60 · SS<60`.
- **Concordance nom-de-fichier** : la date de chaque stamp == la date du **nom du fichier** qui le contient.
- **Non-futur** : aucun stamp ni nom-de-fichier > `2026-08-06` (date harness).
- **Fenêtre du mandat** : ≥ `2026-07-01`.

**Résultat → CLEAN.** **0 anomalie d'intégrité** sur 214 stamps / 37 fichiers : toute date de stamp == la date de son fichier, **0 futur**, **0 date-calendrier invalide**, **0 heure invalide**, **0 hors-fenêtre**. La piste d'audit est calendairement cohérente et non-fabriquée.

**Observation INFORMATIONNELLE (non-défaut, 0 édition).** Le détecteur a d'abord signalé une « non-monotonie » d'ordre dans 6 logs. Investigation : chaque log a **deux régimes** — un bloc **descendant** (newest-first, prepend, en tête) puis un bloc **ascendant** (oldest-first, append, historique en pied). C'est une **convention mixte d'insertion** (prepend récent / append hérité), **pas** une corruption : tous les stamps sont valides, concordants et non-futurs. La contrainte #6 porte sur la **vérité du contenu**, pas sur l'**ordre physique des lignes** → **aucune édition** (réordonner 6 journaux historiques = churn risqué sans gain d'intégrité, cf. #5/#6).

**Dents prouvées (mutants synthétiques en mémoire, prod intouchée).** 6/6 discriminations : le stamp propre `20260806_211659` **épargné (SPARE)** ; 5 classes de mutants **MORDENT** — `20260807_…` (futur → capté comme mismatch, un stamp ne peut être futur sans être aussi ≠ nom-de-fichier daté ; le futur pur du **nom** est capté par l'axe fichier), `20260805_…` dans un fichier `08-06` (wrong-day), `20260231_…` (calendrier invalide), `25:00:00` (bad-hour), `09:61:00` (bad-min). Détecteur non-inert.

**Conformité.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6 — 37/214 recomputés par balayage `git ls-files`) · **0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN, occurrence-zéro, détecteur rejouable `datetime`+`git ls-files` pur). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Fichiers : ce journal + 1 entrée mémoire (`audit-trail-chronology-sweep`).

## Session `20260806_211659` — VÉRIF · axe NEUF « fidélité NUMÉRIQUE côté JS `JSON.parse` : entiers > 2^53 (corruption SILENCIEUSE) + magnitudes hors plage double (→ `Infinity`) » → CLEAN 0/114 · 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 (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire) — le 3e sommet de la trilogie « fidélité `JSON.parse` navigateur ».** Le sweep de ce matin (`20260806_181623`) couvre les sentinelles `NaN`/`Infinity` **non-strictes** : elles lèvent une `SyntaxError` **bruyante** dans le `JSON.parse` de la Console Helios. Restait le sibling **SILENCIEUX**, invisible à toute la chaîne Python : (a) un **entier `abs > Number.MAX_SAFE_INTEGER` (2^53−1 ≈ 9.007e15)** — Python conserve la précision arbitraire et **ne prévient jamais**, mais JS le mappe au double IEEE-754 le plus proche = **valeur silencieusement fausse, sans erreur** ; (b) une **magnitude > `Number.MAX_VALUE` (~1.8e308)** → JS parse en `Infinity`, silencieux. Un id/montant-en-cents/numéro sérialisé en nombre JSON franchissant 2^53 **passe tous les gates** (`validate_json` vert car Python tolère, byte-repro identique) tout en corrompant l'affichage navigateur. Distinct de la FORME scalaire (type int/float) et des sentinelles NaN (valeurs illégales) : ici c'est la **plage sûre du `Number` JS**, jamais éprouvée.

**Détecteur (stdlib pur, rejouable).** Reparse chaque `.json` avec `json.loads(text, parse_int=hook, parse_float=hook)` : flag si `abs(int) > 2^53−1` ou `math.isinf(float(litéral))`. Les numériques-**chaîne** (`"999…"`, légitimes, non affectés par `JSON.parse`) sont épargnés.

**Dents prouvées (8/8 + bout-en-bout).** Épargnés : `18880000`, `2^53−1` exact (MAX_SAFE), `18296296385` (numéro type-téléphone), `"99999999999999999999"` (chaîne), `0.085` (flottant). MORDENT : `2^53`, `2^53+1`, `1e400`→`Infinity`. Bout-en-bout sur `…/faisabilite/bancable/fixtures/brief_bancable.json` : baseline = 0 non-sûr ; injection `_probe = 2^53+7` → **correctement flaguée** `int>2^53` (9007199254740999).

**Résultat.** Sur **114 fichiers JSON** suivis : **0 nombre non-sûr**. Le plus grand entier tracké = **18 880 000** (`brief_bancable.json`) — ~8 ordres de grandeur sous la limite. **CLEAN** — tous les artefacts sont consommables sans perte de précision par le `Number` JS.

**Conformité.** 0 édition de production (sweep read-only). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + 1 entrée mémoire (`js-number-fidelity-sweep`). Sweep **rejouable** — **NON promu en gate** (#5, occurrence-zéro ; note résiduelle : `validate_json` reste int-safe-tolérant par design Python, à durcir seulement si un générateur commence à émettre de grands ids/montants numériques).

## Session `20260806_181623` — VÉRIF · axe NEUF « conformité JSON STRICTE (ECMA-404 / JS `JSON.parse`) : aucun `NaN`/`Infinity` non-strict » → CLEAN · 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 (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Tous les balayages JSON précédents valident le parse via **Python** — `validate_json.sh` fait `json.load(...)`, et Python **ACCEPTE par défaut** les constantes non-standard `NaN` / `Infinity` / `-Infinity` (elles retournent `float('nan')`/`inf`). Or ces tokens sont **illégaux en ECMA-404** : le `JSON.parse` de la Console Helios (frontend JS qui consomme les `out/*.json`) **lève une `SyntaxError`** dessus. Donc un artefact contenant un `NaN` (ex. champ dérivé div-par-zéro sérialisé avec `allow_nan=True`, défaut de `json.dumps`) **passe tous les gates** (`validate_json` vert, byte-repro identique) tout en **cassant le rendu navigateur** — faux-vert direct, invisible à toute la chaîne Python. Distinct de `artifact-internal-set-semantics` (valeurs) et de la « FORME scalaire int/float/str » (qui checke le TYPE, pas les valeurs-sentinelles non-strictes) : ici c'est la **conformité au sous-ensemble JS-parseable**, jamais éprouvée.

**Détecteur (stdlib pur, rejouable).** Re-parse chaque `.json` suivi avec `json.loads(text, parse_constant=<callback qui lève>)` — le callback `parse_constant` n'est invoqué QUE pour `NaN`/`Infinity`/`-Infinity` en position de **valeur** (pas les chaînes `"NaN"`, légales) → réplique exacte de la sémantique de rejet de `JSON.parse`.

**Dents prouvées (synthétique 5/5 + bout-en-bout).** `{"x":1.5}` **épargné** ; `{"x":NaN}`, `{"x":Infinity}`, `{"x":-Infinity}` **MORDENT** ; `{"x":"NaN"}` (chaîne) **épargné** — 5/5 discriminés. Bout-en-bout sur un fichier réel (`…/commissions/bareme.schema.json`) : baseline = strict ; injection `{"_probe":NaN}` → **correctement flaguée** non-strict.

**Résultat.** Sur **114 fichiers JSON** suivis : **0 constante numérique non-stricte**. **CLEAN** — tous les artefacts sont consommables tels quels par `JSON.parse` côté navigateur. Autres divergences Python↔JS déjà couvertes ailleurs : BOM (`runtime-encoding-integrity-sweep` = 0 BOM, or `JSON.parse` rejette le BOM), clés dupliquées (`manifest-count-field…`/dup-keys = 0, or JS garde la dernière). Le delta neuf ici = les **sentinelles NaN/Infinity**.

**Conformité.** 0 édition de production (sweep read-only). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + 1 entrée mémoire. Sweep **rejouable** (stdlib pur) — **NON promu en gate** (#5, occurrence-zéro ; note résiduelle : `validate_json` reste tolérant NaN par design Python, à durcir seulement si un générateur commence à émettre des dérivés flottants risqués).

## Session `20260806_174619` — VÉRIF · axe NEUF « intégrité des ancres-fragment Markdown `](#slug)` : résolvent-elles vers un titre réel ? » → CLEAN · 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 (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Les 7+ derniers commits sont des sweeps vérification-only à rendement décroissant ; `daily_reports` n'a pas de vrai snapshot stakeholder à ajouter. Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Toutes les classes de liens gatées/balayées portent sur la **cible-FICHIER** d'un lien Markdown (`check_docs` §1 = « la cible existe ? », `link-label-target-mismatch` = « le label nomme-t-il la cible ? », `backtick-path-escapes-check-docs` = phantom en code-span). **Aucune** ne valide la **partie fragment** `#slug` : `check_docs.sh` la RETIRE explicitement (`link="${link%%#*}"`) et ignore les ancres pures (`\#*) continue`). Donc un lien `[label](#titre)` — ou `[label](fichier.md#titre)` — dont le titre a été renommé/mal-orthographié **passe au vert** : la cible-fichier existe, seul le saut d'ancre est mort (aveugle au rendu, silencieux). Sœur-FRAGMENT de la classe cible-fichier.

**Détecteur (stdlib, `/tmp`, read-only).** Algorithme de slug GitHub/Gitea (minuscule · ponctuation retirée · accents conservés · espaces→tirets · doublons suffixés `-1`) ; code-spans neutralisés d'abord (comme `check_docs`, sinon faux-positifs) ; toute ancre `](#…)`/`](*.md#…)` résolue contre l'ensemble des slugs-titres du fichier-cible (fichier absent = hors axe, c'est le job de `check_docs`).

**Résultat → CLEAN.** Sur les 379 `.md` trackés, **1** seul lien-ancre réel non-code-span : `README.md:20 [`DIRECTIVE_*.md`](#directives-michel-input-specs-datés)` → résout exactement vers le titre `## Directives Michel (input-specs datés)` (slug avec `é` accentué préservé). **0 ancre pendante.** Faux-positifs correctement écartés = les 2 `` `](#frag)` `` en code-span dans ce même log (prose méta décrivant ce candidat, occurrence-zéro). Détecteur teeth-prouvé bout-en-bout : l'ancre valide résout (True), un mutant-typo `…-date` (sans `s`) NE résout PAS (False = mordrait).

**Périmètre / décision.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6) · **0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN, 1 seule occurrence réelle, détecteur rejouable, comme les précédents sweeps occurrence-zéro). `./run_ci.sh` **33 PASS · 0 FAIL · 0 SKIP** inchangé. Mémoire : `markdown-anchor-fragment-integrity`.

## Session `20260806_171614` — VÉRIF · axe NEUF « intégrité référentielle artefact → FICHIER (les chaînes-chemin des out/*.json résolvent-elles sur disque ?) » → CLEAN 0 pendante · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non
éditables). Le canal `daily_reports` est en retard de 3 sessions **vérification-only** (0 édition prod) —
y ajouter des lignes « CLEAN, 0 édition » serait du churn stakeholder, pas un vrai snapshot. Objectif :
un **axe teeth encore jamais balayé** qui puisse réellement mordre un défaut.

**Axe NEUF (absent de la mémoire).** Toutes les classes référentielles existantes résolvent des **slugs**
(`role-portail-referential-integrity`, `project-entity-referential-integrity`) ou re-dérivent des
**comptes** (`manifest-count-field-payload-rederivation`) ou comparent des **valeurs** entre artefacts
(`cross-artifact-numeric-agreement`). **Aucune** ne traite une **valeur-chaîne d'un out/*.json comme une
RÉFÉRENCE DE FICHIER** et ne vérifie qu'elle **résout sur disque**. Or ces artefacts en portent 124 :
`generated_from`/`source_directive`/`rbac_source`/`workflow_source`/`doctype_source`,
`audited_artifacts[]` (audit_5d), `evidence` (schémas d'audit_4big), `roadmap_ref`, acceptance
`artifacts[].path`, apply_plan `doc`/`produces[]`, MANIFEST `artifacts[]`/`generator`. **Aucun gate ne
parse une valeur JSON comme chemin** (`check_docs` ne voit que les liens Markdown ; `check_artifacts` ne
voit que le byte-repro qui rejoue une chaîne pendante à l'identique). Un `generated_from` pointant un spec
**renommé**, un `audited_artifacts` vers un fichier **déplacé**, un `evidence` schéma **inexistant** =
faux-vert invisible, référence pendante qui contredit #6 « Zéro invention ».

**Détecteur (stdlib pur, rejouable).** Pour chaque valeur-chaîne matchant un motif nom-fichier
(`.json/.md/.py/.sh/…`) : tentative de résolution contre bases par ordre de spécificité — `out/` · dir
module (SRC) · **dir famille** (grand-parent, ex. `crm/`) · racine deliverables · racine dépôt · `rbac/` ;
plus un **fallback bare-basename cross-module** (un spec/schéma référencé par simple basename résout s'il
existe dans un module frère). **Classes conceptuelles/runtime exclues by-design** (n'ont pas à exister
comme fichiers du dépôt) : `capabilities` = noms de capacités OTOIA **VPS** (`aec.py`/`knowledge.py`/
`prompt_engine.py`/`chat.py` — cohérent avec `chat.py` absent du repo, cf. mémoire) · `storage_layout`
PIE = layout de stockage **VPS** (`master.json`/`manifest.json`) · demo `evidence[].file` = chemins
`out/…` **relatifs au module-cible** (pas à demo/scenarios).

**Résultat → CLEAN.** Sur les 57 out/*.json : **48** refs path-relatives + **27** bare-basename
cross-module → **toutes résolvent** vers un fichier trackés ; **49** refs conceptuelles/runtime exclues
(24 capabilities VPS + 23 demo-evidence target-relatives + 2 PIE storage) ; **0 référence pendante**.

**Dents prouvées bout-en-bout.** baseline `commissions/MANIFEST.json generated_from='bareme_spec.json'`
→ résout (non-pendant) ; 4 mutants injectés (`bareme_spec_RENAMED.json`, `DIRECTIVE_TYPO.md`,
`ghost.schema.json`, `crm/nonexist/x_spec.json`) → **les 4 MORDENT** (pendants). Détecteur non-inert.
Limite conservatrice documentée : une pendante par bare-basename **coïncidant** avec le nom d'un autre
fichier tracké serait manquée (accepté — occurrence-zéro).

**Périmètre / décision.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6) ·
**0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN, occurrence-zéro, détecteur rejouable
stdlib pur hors réseau). `./run_ci.sh` **33 PASS · 0 FAIL · 0 SKIP** inchangé (aucune édition de code).
Fichiers touchés : ce journal + 1 entrée mémoire (`artifact-file-reference-integrity`).

## Session `20260806_164612` — VÉRIF · axe NEUF « intégrité de la FORME scalaire JSON (type int-vs-float-vs-str), pas la valeur » des 57 out/*.json → CLEAN · 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 (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Les 6+ derniers commits sont des sweeps vérification-only à rendement décroissant. Objectif : un **axe objectif encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Tous les axes numériques existants portent sur la **valeur** d'un nombre (accord inter/intra-artefact, re-dérivation, arithmétique dérivée) ou sur la **forme-date**. Aucun ne vérifie la **FORME scalaire JSON** elle-même : un compte sérialisé `3.0` (float) ou `"3"` (string) au lieu de `3` (int) **se lit à l'identique** mais casse `type:integer` / une comparaison stricte `== 3` / une colonne DB typée. C'est **aveugle au byte-repro** (build==committed rejoue le type tel quel) et **aveugle aux oracles-schémas** (tous déclarent `type:"number"`, qui accepte int ET float → un `sequence_id` float passe au vert). Sœur-FORME de `cross-artifact-numeric-agreement` (qui, lui, teste la valeur) et de l'axe forme-date.

**Détecteur — 3 sous-checks, teeth-prouvés** (`/tmp`, stdlib pur, hors réseau, rejouable) :
- **(A) cohérence de type intra-fichier par chemin structurel** — un chemin répété (ex. `[].sequence_id`) dont les occurrences divergent de type scalaire = flag. Le regroupement par **chemin structurel complet** (pas le nom-feuille) évite les faux-positifs de collision de nom inter-schéma.
- **(B) clés à sémantique-compte** (`nb_*` / `*_total` / `*_count` / `passed`/`failed`/`ran`…) **doivent** être des int JSON, jamais float ni string-numérique.
- **(C)** aucun littéral `NaN`/`Infinity` (JSON strict invalide).
- **Dents** : basculer **un seul** `sequence_id` int-parmi-des-floats → **MORD** (`{int,float}`) ; `nb_portails=5.0` et `roles_total="44"` → **MORD** ; les comptes réels de `regression_run.json` **épargnés** (0 faux-positif). Détecteurs non-inertes.

**Résultat → CLEAN.** B = 0, C = 0. A ne flague qu'**1** chemin = `run_sheet.json … evidence.[].valeur` (str/int/bool) = la **colonne clé-valeur hétérogène par design** (même piège `valeur` générique déjà en mémoire), **pas un bug** ; les 56 autres fichiers sont parfaitement cohérents en type par chemin. Les divergences de type **inter-FICHIERS** (`role`/`passed`/`verdict`/`is_submittable`/`a_confirmer`/`roadmap_line`) = **collisions de nom de clé per-module-local**, le piège générique documenté (chaque schéma distinct).

**Deux formes float/int DÉLIBÉRÉES — NON-défauts, à NE PAS « corriger » :** (1) `workspace.json sequence_id` = `float(1.0..5.0)` via un cast explicite `float(...)` dans `wslib/frappe.py:162` — **fidélité au champ Float `sequence_id` du Workspace Frappe** ; (2) `is_submittable` = **int 0/1** dans les `doctype_*.json` — convention booléen 0/1 des DocType Frappe. Les deux sont by-design + épinglés byte-repro. Investigués avant conclusion (#5/#6) — pas de correction.

**Périmètre / décision.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6) · **0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN + formes Frappe isolées, comme les précédents CLEAN). `./run_ci.sh` **33 PASS · 0 FAIL · 0 SKIP** inchangé. Détecteur rejouable. Mémoire : `json-scalar-type-integrity`.

---

## Session `20260806_161610` — VÉRIF · axe NEUF « intégrité arithmétique des champs DÉRIVÉS des artefacts committés » (moyennes/taux/sommes-pondérées/écarts/verdicts-seuil + nombres dans la prose) → CLEAN · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non
éditables). Objectif : un **axe teeth encore jamais balayé**. Candidat ancres intra-doc `](#frag)`
ré-écarté (occurrence-zéro, déjà noté). Retenu : la **valeur SÉMANTIQUE** d'un nombre DÉRIVÉ.

**L'axe (distinct de tout ce qui existe).** Les balayages numériques passés couvrent la **cardinalité**
(`manifest-count-field-payload-rederivation` : `nb_*`/`_total` = len(payload)), l'**accord inter-artefacts**
d'un singleton (`cross-artifact-numeric-agreement`), et les **doublons d'ensemble** (`artifact-internal-set-semantics`).
Aucun ne recalcule une **DÉRIVATION arithmétique** — un nombre stocké = *fonction* d'autres nombres stockés
(moyenne, taux %, somme pondérée, écart, verdict-seuil). C'est aveugle au byte-repro **d'une façon subtile** :
`check_artifacts` (et le test `test_build_egale_commite` du module financement) comparent la sortie du
générateur au **golden committé** — donc ils attrapent la *dérive* (code changé sans régénérer) mais **PAS**
un bug de formule **régénéré en même temps** (code + golden bougent ensemble → toujours vert). Un
`overall_percent` faux montré au **banquier** (roadmap « P07 pitch banquier ») serait un faux-vert vs #6.
Cet axe **recalcule depuis les entrées brutes**, indépendamment du générateur.

**Faux-piste corrigée en cours de route (honnêteté).** J'avais d'abord supposé `overall_percent` non-testé
(grep : 0 assert). Mutation-test de `finlib/gate.py` (mean `/len→/3.0`, doc pct `*100→*90`, f-string
`paid/required` inversé, chacun in-place puis `git checkout`) → **chaque mutant FAIL** via
`test_build_egale_commite` (rebuild == golden byte-à-byte). Donc l'arithmétique **est** protégée-dérive.
La vraie lacune résiduelle = le bug-de-formule-avec-régén, que **seul un recalcul premier-principe** ferme.

**Recalcul premier-principe → CLEAN partout :**
- **financement `gate_status_example.json`** : `overall_percent` = moyenne(4 %) = **39.1** ✓ ; 3 % par
  condition (`paid/required`, `validated/total`, `signed/total`, arrondis `_round1`) ✓ ; `remaining` =
  `required−paid` = 14000 ✓ ; les **5 nombres embarqués dans la prose `reasons`** (`29500/43500`, reste
  `14000`, `67.8%`, `5/13`, `2/4`) miroir des champs structurés ✓.
- **audit_4big `quality_report.json`** : pour les **24 modules** `applicable_weight` = Σ(poids checks),
  `earned_weight` = Σ(poids passés), `score` = round(earned/applicable×100), `verdict` = PASS ssi score≥95 ✓ ;
  `totals` (pass/fail/min/max/`pass_rate_pct` = round(pass/modules×100)) ✓.
- **financement `banques.json`** : 6× ordre LTV `0≤ltv_min≤ltv_max≤100` ✓.
- **commissions** : bornes de taux `[0,100]` ✓.

**Dents prouvées bout-en-bout.** Détecteurs de recalcul mordent sur mutants injectés dans une **copie**
de l'artefact : `overall_percent 39.1→52.1` **MORD**, prose `paid 29500→99999` **MORD**, audit_4big
`score→77` **MORD** ; l'artefact réel passe. Détecteurs stdlib pur (`json`), sans réseau, rejouables.

**Décision.** **NON promu en gate** (#5) : les nombres dérivés sont déjà protégés-dérive par
`build==committed`/`check_artifacts` ; cet axe ferme la classe résiduelle (formule-avec-régén) par un
sweep **rejouable**, occurrence-zéro. **0 édition de production** (mutations `gate.py` toutes restaurées
par `git checkout` — arbre propre), **0 commande VPS** (#8). `run_ci` inchangé 33/0/0.


## Session `20260806_151604` — VÉRIF · axe NEUF « intégrité référentielle projet-code ↔ projet-nom canonique dans les artefacts » → CLEAN 128/128 · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit de Michel
(non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** `role-portail-referential-integrity-sweep` prouve que tout **slug
de rôle/portail** des `out/*.json` résout vers `rbac_50_roles.json`. Le pendant jamais balayé sur la
**dimension PROJET/ENTITÉ** : tout **code-projet** (`P0[0-9]`) et **nom-projet** référencés dans les
artefacts résolvent-ils vers le canon `CLAUDE.md §Projets`, et quand un objet **apparie** un code et un
nom, l'appariement est-il le bon ? Distinct de `claude-md-constant-anchor-gate` (qui gate des **copies**
de la liste §Projets) : ici on chasse la **référence pendante / le mauvais appariement** (une invention
`P10`, ou `P02 « Nakua »` — P02 = Coral del Sur) — un faux-vert invisible à tous les gates et qui
contredirait directement la contrainte **#6 « Zéro invention »**.

**Détecteur (stdlib pur, rejouable).**
- Canon : `{P01 Structure, P02 Coral del Sur, P03 Nakua, P04 Xamaná Cantiles, P05 Las Colinas Najayo
  Arriba, P06 Coco Real, P07 Aqua Terra Las Terrenas, P08 Fasano Espirilla, P09 1069 Crisfer}`.
- **Invariant fort (appariement objet)** : pour chaque objet JSON dont les valeurs-chaînes contiennent
  **exactement un** code ET **exactement un** nom canonique, le couple `(code, nom)` doit être
  canonique. Le « exactement 1+1 » exclut proprement les blobs de prose (voir faux-positifs).
- **Chaîne-agrégat** : l'unique objet multi-codes (`dossier_vente.options` = les 9 paires inline
  `"P0X <Nom>"`) vérifié ligne à ligne = 9/9 canoniques (déjà gaté §Projets).
- **Pendantes** : aucun token `P[01][0-9]` hors `{P01..P09}`.
- **Entités** : les 8 de `§Entités` (WAF · WA SRL · AC Arias Cuevas · Consortium ECR DR · Helios RD ·
  WAG · Ploutos · 9060 QC) résolvent toutes (WA SRL 26× · Helios RD 23× · AC Arias Cuevas 21× · WAG 7× ·
  9060 QC 6× · Ploutos 5× · Consortium ECR DR 5× · WAF 4×) — 0 entité inventée.

**Dents prouvées (mutants sur copie temp, prod intouchée).** baseline `pie_manifest.json` = 0 mismatch ;
mutant `P02 ← Nakua` → **MORD** `[('P02','Nakua')]` ; mutant `P02 → P10` → pendante **flaguée**
`['P10']`. 3/3 discriminés.

**Résultat.** Sur les 57 `out/*.json` : **128 objets 1:1 code+nom → 0 mismatch** · 1 agrégat (les 9
paires inline) 9/9 canonique · **0 code pendant** · 8/8 entités résolvent. Faux-positifs écartés (par
construction du détecteur objet) : 5 adjacences **inline en prose** (`"P07 Aqua Terra — Pitch
banquier"`, `"P07 Aqua Terra faisabilité…"` dans `run_sheet`/`acceptance_matrix`/`MANIFEST`) utilisent
la **forme courte** « Aqua Terra » (abréviation de « Aqua Terra Las Terrenas ») ou **deux codes** dans
une phrase — pas un mauvais appariement, prose légitime que l'invariant « exactement 1 nom **complet** »
exclut correctement.

**Conclusion.** **CLEAN 128/128** (+ agrégat 9/9 + 0 pendant + 8/8 entités). Axe teeth-prouvé, dimension
projet/entité jamais balayée (sœur de la dimension rôle/portail). **0 édition de production** (#8),
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + 1 entrée mémoire
(`project-entity-referential-integrity-sweep`). Sweep **rejouable** (stdlib pur, sans réseau) — **NON
promu en gate** (#5 : appariement partiellement déjà couvert §Projets, occurrence-zéro sur la pendance).

## Session `20260806_144555` — VÉRIF · axe NEUF « forme de normalisation Unicode (NFC vs NFD) des sources committées » → CLEAN 387/387 · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit de Michel
(non éditables). Objectif : un **axe encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Le sweep `runtime-encoding-integrity-sweep` a couvert le niveau
octet — BOM · UTF-8 invalide · caractères de contrôle · CRLF · compile `.py`. Il n'a **jamais** couvert
la **forme de normalisation Unicode**. Or ce repo est **intégralement francophone** (369/387 fichiers
non-ASCII, pleins d'accents) : un `é` **décomposé** (NFD = `e` + U+0301 combinant) se **lit à
l'identique** d'un `é` **composé** (NFC, un seul point de code), mais c'est une **séquence d'octets
différente**. Conséquences d'un fichier NFD glissé dans le repo :
- **byte-repro aveugle** — le générateur lit ces octets et les ré-écrit tels quels → artefact identique.
- **faux-vert de gate** — un gate qui `grep` un littéral **NFC** (ex. `faisabilité`, `Bancable`) ou un
  test qui `assert "…é…" in output` **rate silencieusement** le contenu NFD → la vérif passe au vert
  alors qu'elle ne teste rien.

**Détecteur (stdlib pur).** Pour chaque fichier texte committé (`.py .json .md .sh .yml .yaml .txt
.ts .tsx .js .jsx .toml .cfg .ini`, hors `.git/__pycache__/node_modules/.expo/venv`) : décoder UTF-8,
comparer `unicodedata.normalize("NFC", texte)` au texte brut ; toute divergence = contenu non-NFC.

**Teeth (bout-en-bout, pas juste abstrait).** Deux fichiers temporaires de contenu **visuellement
identique** — `clean.md` (NFC) et `decomposed.md` (NFD de la même phrase) : le détecteur **flague**
`decomposed.md` (True) et **épargne** `clean.md` (False) ; et `grep 'faisabilité'` (littéral NFC)
matche `clean.md` **mais rate `decomposed.md`** → le faux-vert est **démontré**, pas supposé.

**Résultat.** **387 fichiers** examinés · **369** non-ASCII (accentués) · **0 non-NFC** · 0 échec de
décodage. Le repo est **intégralement en forme NFC** → **CLEAN 387/387**. Aucun gate/test n'est exposé
au faux-vert d'octets-divergents-lus-pareil.

**Périmètre.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** · **0 commande
VPS** (#8). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers touchés : ce journal +
1 entrée mémoire (`unicode-nfc-normalization-sweep`). Sweep **rejouable** tel quel (stdlib
`unicodedata`, sans réseau) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_141554` — VÉRIF · axe NEUF « accord numérique INTER-artefacts d'un concept-singleton partagé » → CLEAN · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit de Michel
(non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Les axes numériques existants sont soit **intra-artefact**
(`[[manifest-count-field-payload-rederivation]]` : chaque `MANIFEST.nb_*` re-calculé depuis SON payload
voisin), soit **code/prose ↔ artefact** (`[[doc-numeric-claims-gate]]`, `[[prose-facts-vs-numeric-drift]]`).
Angle orthogonal jamais couvert : **artefact ↔ artefact** — quand un même *concept-singleton* plateforme
apparaît comme scalaire dans **deux `out/*.json` indépendants**, les deux valeurs s'accordent-elles ?
Une divergence silencieuse (un manifest dit 50, un autre 44) *ship* verte car aucun gate ne compare deux
artefacts entre eux.

**Détecteur (teeth-prouvé).** Balayage des **57 `out/*.json`** : collecte des clés-scalaires int en
**position GLOBALE** (chemin sans indice de liste `[i]` = concept singleton, ≠ position PER-ROW dans un
`[i]` = compte-de-ligne légitimement variable). Une clé globale partagée par >1 fichier avec valeurs
divergentes = candidat-conflation. **Preuve de dents** : mutation `cible_rbac_roles` 50→49 dans un seul
manifest en mémoire → le détecteur **signale** aussitôt la divergence (BITES).

**Résultat empirique → CLEAN.** Les concepts-singletons plateforme s'accordent tous cross-fichier :
`cible_rbac_roles`=50 (4 fichiers), `ci_modules_count`/`modules`/`suites`=24 (3), `portails`=5,
`doctypes_uniques`=45, `custom_docperm`=116, `user_permission_templates`=28, `role_profiles`=6,
`min_score`=100, `pass_score`=95 — zéro divergence.

**TRAP documenté (comme les KEY-traps de `[[manifest-count-field-payload-rederivation]]`).** L'heuristique
naïve « même nom de clé = même concept » **sur-signale** : `fields`/`data_fields`/`permissions`/`roles`/
`sections`/`pass` divergent entre modules (`dossier_vente` 30 fields vs `confotur` 18 ; `audit_4big`
pass=24 vs `audit_5d` pass=13) — mais chacun décrit **son propre DocType/suite** = per-module-local, PAS
un singleton plateforme. Le seul **nom SURCHARGÉ** portant deux sens cross-famille est `roles_couverts`
(**50** dans `rbac/roleprofile_gen` vs **44** dans `frontend/portails`, `frontend/chat_otoia`,
`mobile/app_config`) — et c'est **by-design**, les DEUX re-dérivés du canonique `rbac_50_roles.json` :
- **50** = tous les rôles reçoivent un role-profile (`len(roles)`=50) ;
- **44** = rôles surfacés dans les **5 portails business** (ventes 12 · construction 10 · compta 8 ·
  direction 9 · achat 5) = 50 − **6** rôles `plateforme` (`devops`, `rbac-admin`, `qa`, `otoia`,
  `mobile`, `bi`) sans portail business. Cohérent avec `[[role-portail-referential-integrity-sweep]]`,
  `[[roles-couverts-transitively-single-sourced]]`, `[[roadmap-7-dashboards-not-drift]]` (les 6 `plateforme`).

**Décision.** **Aucune édition de production** (l'axe est CLEAN) · **aucun gate ajouté** (#5 —
occurrence-zéro d'une vraie conflation ; détecteur rejouable, non-déterministe-safe) · aucune commande
VPS (#8). `run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Canal stakeholder `daily_reports/2026-08-06.md`
porté au HEAD (currency `3710253`→`04f4c21` + cette session).

## Session `20260806_134554` — VÉRIF · axe NEUF « l'artefact COMMITÉ sur disque conforme à son schéma ? » (fichier réel vs oracle, pas l'objet en-mémoire) → CLEAN 57/57 · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit appartenant à
Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Les 7 axes-schéma existants (non-vacuité, closedness, wiring, `$ref`,
enum/const, cohérence interne, draft/keyword) examinent tous **le schéma**. La mémoire note un angle
jamais fermé : « *jsonschema oracles validate in-memory objects not flat out/ (no flat pair = 0-err,
by-design)* ». Question NEUVE et orthogonale : le **fichier `out/*.json` réellement commité et livré**
est-il conforme à son schéma — ou une régression de forme peut-elle *ship* verte parce que l'oracle ne
valide qu'un objet reconstruit en mémoire de forme différente ? Détecteur (jsonschema 3.2.0) : pour
chacun des **57 `out/*.json`**, valider le fichier sur disque contre le schéma du module — soit comme
objet-entier, soit comme **valeur d'une propriété top-level** du bundle validé.

**Résultat empirique.** 5 fichiers valident **WHOLE** (pie_manifest, quality_report, audit_report,
regression_plan, + MANIFEST-scénario) ; la majorité valident comme **propriété de bundle** (le générateur
valide le bundle entier en mémoire puis écrit `bundle["<prop>"]` dans un fichier split). **4 « no-fit »**
investigués un par un — **tous by-design & couverts transitivement** :
- `crm/workflow_vente/out/workflow.json` : le générateur écrit `[bundle["workflow"]]` (l.192) — **wrap
  liste 1-élément = convention fixtures Frappe** ; l'objet interne EST validé (prop `workflow`) et le
  wrapper est byte-repro-épinglé (check_artifacts).
- `qa/acceptance/out/acceptance_matrix.json` = bundle `{matrix,verdict}` **moins** `manifest` (extrait
  vers MANIFEST.json) ; sous-parties validées en mémoire + byte-repro.
- `demo/scenarios/out/run_sheet.json` : vue rendue distincte (`scenario.schema.json` garde en fait
  **MANIFEST.json**) ; sortie `build` déterministe byte-repro'd + structure assertée par
  `check_invariants` (lit `run_sheet["scenarios"]`, `sc["id"]`…).
- `qa/regression/out/regression_run.json` : snapshot d'EXÉCUTION (sous-ensemble du schéma-plan) — **seul
  artefact hors byte-repro de check_artifacts**, MAIS forme épinglée autrement : `ci/check_regression.sh`
  **ré-exécute `run` et byte-compare** au fichier commité (l.53-70). Aucune dérive de forme ne passe.

**Chaîne de garde fermée.** Tout payload commité livré a sa forme gardée par une chaîne traçable :
`schéma-whole` · `schéma-prop + byte-repro` · `wrap/split byte-repro d'un objet schéma-validé` ·
`ré-run byte-compare`. **0 fichier livré à forme non-gardée.** Le trou « in-memory vs commité » noté en
mémoire est comblé par byte-repro / ré-exécution, vérifié sur les 57.

**Dents prouvées.** Sur un fichier qui matche une prop (`workflow_state.json`→`workflow_state`) : mutation
clé-parasite (props en `additionalProperties:false`) → **no-fit** ; mutation type-scalaire → **no-fit**
(2/2 MORDENT). Le détecteur discrimine une divergence de forme réelle.

**Conclusion.** **CLEAN 57/57.** Axe teeth-prouvé, **rejouable** (`/tmp` jsonschema pur, hors réseau,
hors prod · #8). **0 édition de production**, 0 artefact touché, `./run_ci.sh` inchangé = **33 PASS ·
0 FAIL · 0 SKIP**. **NON promu en gate** (#5, occurrence-zéro ; la chaîne byte-repro/ré-run garde déjà
la forme). Fichiers touchés : ce journal + 1 entrée mémoire (`committed-artifact-conforms-to-schema`).

## Session `20260806_131551` — VÉRIF · axe NEUF « cohérence draft/keyword des 26 oracles-schémas » (déclaration `$schema` vs idiomes cross-draft) → CLEAN 26/26 · 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 (Sprints 1→7 dans le repo ; prod VPS /
builds EAS / dépôts ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages
**produit appartenant à Michel** (non éditables). Objectif anti-churn : un **7ᵉ axe teeth des
oracles-schémas encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Les 6 axes-schémas déjà en mémoire vérifient : (1) non-vacuité ·
(2) fermeture/complétude · (3) câblage à un test · (4) résolution des `$ref` · (5) enum/const vs skip
`python -S` · (6) satisfiabilité interne (required-vacuité + auto-contradiction). **Aucun** ne vérifie
la **cohérence entre le `$schema` déclaré et les mots-clés effectivement employés**. C'est un faux-vert
réel : un validateur JSON-Schema interprète chaque schéma **selon le draft déclaré**, et un mot-clé
d'un autre draft change de sémantique ou est **silencieusement ignoré** — l'oracle valide alors autre
chose que ce qu'il croit.

**Détecteur maison (stdlib pur, récursif).** Signale, sous une déclaration `draft-07` :
`exclusiveMinimum`/`exclusiveMaximum` **booléens** (idiome draft-04 → mal-typé et ignoré en draft-07) ·
`id` au lieu de `$id` (draft-04) · `required` **booléen** (draft-03) · `$defs` comme porteur de
définitions (mot-clé draft-2019, alors que draft-07 utilise `definitions`) · les mots-clés
**post-draft-07** (`unevaluatedProperties/Items`, `$dynamicRef`, `$anchor`, `prefixItems`,
`dependentRequired`, …) **silencieusement ignorés** par un validateur draft-07.

**Résultat.** **26/26 déclarent `draft-07`** (aucun `$schema` manquant) · **13/13** porteurs de
définitions utilisent `definitions` (jamais `$defs`) · **0 idiome booléen draft-04** · **0 mot-clé
post-draft-07**. → **CLEAN 26/26.**

**Dents prouvées.** Copies-mutant en mémoire de `rbac.schema.json` (réel = 0 hazard) : `exclusiveMinimum:true`,
`id:"http://…#"`, `required:true`, `$defs:{…}`, `unevaluatedProperties:false` → détecteur **MORD 5/5**.
Les 26 schémas réels restent CLEAN → détecteur non-inerte.

**Conclusion.** **CLEAN 26/26** sur un axe de cohérence draft/keyword jamais couvert par les 6 axes
existants. **0 édition de production** (#8), `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**.
Fichiers touchés : ce journal + 1 entrée mémoire (`schema-oracle-draft-keyword-coherence`). Sweep
**rejouable tel quel** (stdlib pur, sans réseau ni jsonschema) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_121542` — VÉRIF · axe NEUF « cohérence interne des 26 oracles-schémas » (required-vacuité + auto-contradiction const/enum/default) → CLEAN · 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 (Sprints 1→7 dans le repo ; prod VPS /
builds EAS / dépôts ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages
**produit appartenant à Michel** (non éditables). Objectif anti-churn : un axe teeth des oracles-schémas
encore **jamais balayé**.

**Axe NEUF (absent de la mémoire).** Les 5 axes-schémas déjà en mémoire vérifient : (1) non-vacuité
(`schema-oracle-teeth-audit`) · (2) fermeture/complétude (`schema-oracle-closedness-not-defect`) ·
(3) câblage à un test (`schema-oracle-wiring-axis`) · (4) résolution des `$ref`
(`schema-oracle-ref-integrity`) · (5) enum/const vs skip `python -S` (`schema-oracle-enum-const-vs-S-skip`).
Aucun ne vérifie la **cohérence LOGIQUE INTERNE d'un nœud** : un schéma peut être clos, non-vacuous en
surface, `$ref`-sain ET pourtant contenir un nœud **impossible à satisfaire** (donc faux-vert : le test
qui valide un objet construit en mémoire ne le remarque jamais si l'objet n'exerce pas ce nœud).

**Deux détecteurs maison (stdlib pur, marchent partout, sans réseau ni jsonschema).**
1. **required-vacuité** — un nœud objet avec `additionalProperties:false` ET une clé `required` **absente
   de `properties`/`patternProperties`** est **inatteignable** (aucun objet ne peut valider). Marche
   récursif sur tous les mots-clés porteurs de sous-schéma (`properties`, `items`, `$defs`, `allOf`,
   `anyOf`, `oneOf`, `if/then/else`, `additionalProperties`-objet, …). `additionalProperties`-objet
   ou absent (défaut `true`) ⇒ **non** signalé (la clé peut se satisfaire hors `properties`) = conservateur.
   → **1405 nœuds balayés · 0 vacuité.**
2. **auto-contradiction** — `const`, chaque membre `enum`, et `default` sont confrontés à leurs
   contraintes **sœurs** (`type`, bornes numériques `minimum`/`maximum`/`exclusive*`, `pattern`,
   `min/maxLength`) ; plus les plages absurdes (`minimum>maximum`, `minItems>maxItems`). Un `const` qui
   viole son propre `type`, un `default` hors `pattern`, un membre d'enum sous le `minimum` = données
   invalides latentes / nœud mort. → **317 constraint-checks · 0 contradiction.**

**Dents prouvées.** Copies-mutant dans `/tmp` : `required:["ghost"]`+`additionalProperties:false` →
détecteur 1 **MORD** ; `const:"notint"` sous `type:integer`, membre enum `5` sous `minimum:10`,
`default:"abc"` hors `pattern:^[0-9]+$`, `minimum:100/maximum:1` → détecteur 2 **MORD** (4/4). Les vrais
26 schémas restent CLEAN sur les deux → ce n'est pas un détecteur inerte.

**Conclusion.** **CLEAN 26/26** sur deux sous-axes de cohérence interne jamais couverts. Aucun faux-vert
d'oracle de ce type. **0 édition de production** (#8), `./run_ci.sh` inchangé = **33 PASS · 0 FAIL ·
0 SKIP**. Fichiers touchés : ce journal + 1 entrée mémoire (`schema-oracle-internal-consistency`). Sweep
**rejouable tel quel** (stdlib pur) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_101528` — VÉRIF · axe NEUF « fermeture & complétude des 26 oracles-schémas » → 1 anomalie investiguée (financement) = NON-DÉFAUT (choix explicite + couverture transitive byte-repro) · 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 (repo Sprints 1→7 ; prod VPS / builds
EAS / dépôts ONAPI hors périmètre worker · #8). Registre `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages
produit **appartenant à Michel** (D-01 PIE `contrats` · D-02/V10 financement audit IA · D-03 chat.py),
non éditables par le worker. Objectif anti-churn : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire) → « fermeture (`additionalProperties:false`) & complétude des
`properties` » des 26 oracles.** Distinct de `schema-oracle-teeth-audit` (qui prouvait la
**non-vacuité** : required + ≥2 mots-clés de contrainte). Ici la question est **complémentaire** :
un schéma non-vacuous mais **ouvert** (`additionalProperties` ≠ `false`) OU dont les `properties`
n'énumèrent qu'un **sous-ensemble** des clés réellement produites laisse passer des clés
**fantômes/renommées/droppées** — l'oracle serait alors partiellement aveugle sur la *forme*.

**Méthode.** (1) Balayage des 26 schémas : à chaque nœud-objet, `additionalProperties is False` ? →
**23/26 entièrement fermés** (convention dominante). (2) Pour les modules reconstructibles, diff
`clés du bundle réel` ⊆ `properties déclarées`. Confirmation authoritative pour financement via
`build_bundle(spec, RoleResolver.from_path())` (le vrai bundle, pas une reconstruction).

**Résultat — 1 seule anomalie réelle : `crm/financement_bancaire/financement.schema.json`.**
- Top porte `"additionalProperties": true` **écrit explicitement** (les 3 pairs CRM —
  `commissions`/`dossier_vente`/`workflow_vente` — sont 100 % fermés).
- Les `properties` n'énumèrent qu'un sous-ensemble : **8 nœuds** émettent des clés non déclarées
  (`manifest.titre/source_version/source_directive/rbac_source`, `counts.documents_requis_*`,
  `banques[].note`, `sections[].resume`, `gate_spec.endpoint/submit_endpoint/banniere.couleurs`,
  `gate_status_example.projet/unite/banque_choisie`). Le validateur **maison**
  (`publiciste/lib/validator.py`) honore pourtant `additionalProperties` (support confirmé).

**Verdict : NON-DÉFAUT — souplesse délibérée + transitivement couverte.** (a) Le schéma valide
**serré** les champs structurels (ids `pattern ^[a-z0-9_]+$`, `enum` catégories/applies_to, `counts`
`minimum/maximum`, tableaux `minItems/maxItems 4`) et laisse **libres** les seuls champs
**descriptifs** (`note`, `resume`, `titre`, `endpoint`, `couleurs`) — **même motif que `seo`**
(`schema_org.@graph[]` JSON-LD intrinsèquement variable + `manifest` descriptif ouverts par design ;
`brief.schema.json` ouvert = schéma d'**ENTRÉE**). (b) Ces 8 champs sont **épinglés au byte près** par
`test_build_egale_commite` (l'artefact `out/` commité == build frais) : toute dérive de nom/présence
sur la spec fixe est déjà **RED** via `check_artifacts`, indépendamment de la souplesse de l'oracle.
→ Fermer/compléter le schéma serait un **churn** sur un fichier au choix explicite (#5), à risque de
faux-rejet mal typé, sans dents nettes gagnées (spec figée + byte-repro).

**Faux positif écarté (mobile).** `mobile/app_config/mobile.schema.json` top = `additionalProperties:
false` (fermé) ; les 4 « under-spec » signalés par le proxy = artefacts de ma **reconstruction plate**
(objets descriptifs imbriqués `store_listing`/`app_config` que le vrai bundle nidifie autrement).
Balayage `additionalProperties` confirmait déjà `open_with_props=0` → mobile **entièrement fermé**.

**Portée / anti-churn.** **0 fichier de production édité**, **0 schéma modifié**, **0 artefact
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `26`/`23`/`8` recomputés
par balayage), **0 commande VPS** (#8). Balayage lecture-seule, occurrence-zéro de défaut, rejouable
→ **pas un gate**, sibling de `schema-oracle-teeth-audit` / `verify-non-defects-reconciliation-traps`.
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé, aucune édition de code). Seules éditions :
ce journal + mémoire `schema-oracle-closedness-not-defect` (+ pointeur `MEMORY.md`).

## Session `20260806_094524` — VÉRIF indépendante · NOUVEL axe teeth « tests silencieusement fantômes (shadowing) » → CLEAN 25/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 (Sprints 1→7 côté repo ; prod VPS /
builds EAS / dépôts ONAPI hors périmètre worker · #8). Les ~15 derniers commits sont des sweeps
vérification-only à rendement décroissant. Objectif : chasser un **vrai défaut ungaté** sur un **axe
teeth encore jamais balayé** (anti-churn).

**Axe NEUF (absent de la mémoire) → famille « tests silencieusement fantômes ».** Distinct de
`module-suite-teeth-mutation` (mute la LOGIQUE générateur) et `schema-oracle-teeth-audit` (audite
l'ORACLE) : ici la cible est le **harnais de test lui-même** — 3 cas où une méthode de test est
*définie* mais `unittest discover` ne l'exécute **jamais**, sans qu'aucun gate ne détecte la perte :
- **`def test_x` dupliqué dans une classe** — Python garde la **dernière** liaison ; la 1ʳᵉ ne tourne jamais.
- **`class TestFoo` dupliqué dans un fichier** — la 2ᵉ classe masque la 1ʳᵉ ; les tests de la 1ʳᵉ ne tournent jamais.
- **`def test_*` au niveau module** (hors TestCase) — `discover` ne le lance pas, mais un compteur textuel le tallye.

**Pourquoi c'est un vrai trou (aveugle aux autres sweeps).** Un shadow fait **passer le test par
absence** ; et `check_readme_claims` (classe `doc-numeric-claims-gate`) compte les `def test_`
**textuellement** → un shadow ferait **SUR-compter** le README vs le compte runtime réel (dérive
inverse de l'habituelle). byte-repro / forward-compat / mutation-de-suite sont tous aveugles.

**Détecteur (AST) — teeth-prouvé.** Parse de chaque `git ls-files '*test*.py'` : par ClassDef,
`Counter` des méthodes `test*` > 1 ; par fichier, `Counter` des noms de ClassDef > 1 ; corps de
module, `def test_*` = niveau-module. **Sonde synthétique 3-cas → les 3 MORDENT** (méthode-dup,
classe-dup, niveau-module). Balayage réel → **CLEAN 25/25** fichiers de test (0 méthode-dup,
0 classe-dup, 0 test niveau-module).

**Verdict.** Repo **CLEAN** sur le nouvel axe. **Aucun défaut de production.** Ces dents sont
réellement gardées par aucun gate (un test shadow serait détecté ici et nulle part ailleurs), mais
l'occurrence-zéro ne justifie **pas** un gate permanent (#5) : consigné comme sweep **rejouable**
(mémoire `test-shadowing-sweep`), sibling de `runtime-encoding-integrity-sweep` / `hash-seed-…`.

**Portée / anti-churn.** **Production éditée : néant.** Édité : ce journal + 1 entrée mémoire.
**0 artefact reconstruit**, **0 gate ajouté** (#5), **0 chiffre inventé** (#6 — 25/25 recomputés par
balayage), **0 commande VPS** (#8). Structure du gate inchangée (33 checks).

## Session `20260806_091519` — FIX doc · NOUVEL axe objectif « cohérence du compte de colonnes des tables Markdown » → 1 vrai défaut (ligne à colonne orpheline dans le TEMPLATE canonique de faisabilité)

**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 (Sprints 1→7 côté repo ; prod
VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8). Les ~12 derniers commits sont des
sweeps vérification-only à rendement décroissant. Objectif : chasser un **vrai défaut ungaté** sur
un **axe objectif encore jamais balayé** (anti-churn), sinon améliorer une doc (fallback).

**Axe NEUF (absent de la mémoire) → défaut réel.** **Cohérence du compte de colonnes des tables
Markdown** : une ligne de corps dont le nombre de cellules `|` diffère de l'en-tête rend
**désalignée** (cellule orpheline). Distinct des sweeps encodage/liens/backtick déjà en mémoire.
Balayage de tous les `.md` (`git ls-files '*.md'`, détection en-tête↔séparateur↔corps, `|` échappés
ignorés) → **3 hits**, dont **2 faux positifs** (fichiers-snapshots **historiques**
`2026-08-05.md` avec un `|` littéral **dans un code-span** `` `"|"` `` — immuables, non touchés) et
**1 vrai défaut**.

**Défaut (réel, haute confiance).**
`05_deliverables_mvp/faisabilite/TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md` §5.2 « Financier
bancable » — table à en-tête **4 colonnes** (`| # | Champ | Oblig. | Valeur canonique / placeholder |`).
Les lignes 4.4→4.12 ont toutes **4** cellules (valeur+placeholder fusionnés dans la 4ᵉ colonne,
ex. 4.4 `` `{{cout_construction}}` USD ``). La ligne **4.13** en portait **5** : elle scindait
`CONFOTUR · Fideicomiso (voir volet juridique)` **et** `` `{{regime_fiscal}}` `` en deux cellules
→ colonne orpheline visible pour cette seule ligne, dans un **template CANONIQUE** que le
générateur 4 volets, le Publiciste et la régénération batch S4 (§8) doivent reproduire.

**Fix.** Fusion des deux fragments de 4.13 dans l'unique 4ᵉ colonne avec `·`
(`CONFOTUR · Fideicomiso (voir volet juridique) · `` `{{regime_fiscal}}` ``), idiome des lignes
sœurs (placeholder inline dans la cellule « Valeur »). `regime_fiscal` reste un champ légitime
consommé par `genlib/renderer.py:195` + `banclib/finance.py`.

**Sûreté vs gate.** `ci/check_readme_claims.sh:5765` **lit** ce template mais n'ancre que les lignes
#9 (l.144-146 `**N %** (fixe · contrainte #9)`) et #10 (l.149-151) ; la ligne 4.13 ne porte
**ni** `#9` **ni** `#10` **ni** pourcentage en gras → aucune regex du gate ne la touche.
**Vérifié** : les **5** surfaces d'ancrage du template restent **vertes** post-fix.

**Vérif.** Re-balayage tables → ligne 4.13 **CLEAN** (restent les 2 FP historiques) ·
`ci/check_readme_claims.sh` exit=0 · `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**.

**Portée / anti-churn.** Édité : le template (1 ligne) + ce journal + 1 entrée mémoire. **0 artefact
reconstruit** (le template n'est pas un `out/*.json`), **0 gate ajouté** (#5 — origine isolée, une
seule occurrence réelle repo-wide ; sweep rejouable, pas un contrat à figer, cf. `link-label-target
-mismatch`), **0 chiffre inventé** (#6), **0 commande VPS** (#8). Structure du gate inchangée (33 checks).

## Session `20260806_054454` — FIX doc · lien Markdown label↔cible incohérent (label = chemin de fichier, href pointant ailleurs) → 3 corrigés

**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+ derniers commits** sont des sweeps vérification-only
(`0 édition`) — rendement décroissant, du churn. Objectif : trouver un **défaut réel actionnable**
sur un axe **non couvert** par la mémoire, sinon améliorer une doc (fallback).

**Axe éliminé (CLEAN, non re-flaggé).** Chasse aux **chemins-fantômes dans les code-spans backtick**
(classe `backtick-path-escapes-check-docs` — les backticks neutralisent le link-check). Résolution
contre racine dépôt **+ racine deliverables + sous-arbres modules**, en excluant les snapshots
historiques (`05_activity_log/`, `daily_reports/`), les globs `*/AGENT.md` et les chemins
runtime/VPS (`otoia/`, `out/`, `vente.otov7.com`). Les 2 candidats survivants
(`crm/commissions/README.md` `banclib/finance.py` · `onapi_legal/AGENT.md` `cflib/*.py`) sont des
**faux positifs** : le premier est un **label** de lien dont la vraie cible
`../../faisabilite/bancable/banclib/finance.py` **existe** ; le second résout sous
`legal/confotur/cflib/`. → dimension **CLEAN**, mémoire confirmée.

**Axe NEUF (absent de la mémoire) → défaut réel.** Liens Markdown de la forme `[`label`](cible)`
où le **label** (code-span) est un **chemin de fichier existant** mais dont le **basename diffère**
de celui de la **cible** href. `check_docs` prouve que la cible **résout**, **jamais** que le label
la **désigne** : un lecteur qui Cmd-clique `ci/guard_constraints.sh` atterrit sur `ci/README.md`
(mauvais fichier). Balayage `git ls-files '*.md'` → **3 occurrences**, toutes dans `README.md` :
- L54 `[`ci/guard_constraints.sh`](ci/README.md)` et `[`ci/check_docs.sh`](ci/README.md)`
- L75 `[`ci/guard_constraints.sh`](ci/README.md)`

Les scripts nommés **existent et sont git-trackés** (ce sont des gates) ⇒ **FIX** = pointer chaque
label vers le fichier qu'il **nomme** (`ci/guard_constraints.sh` / `ci/check_docs.sh`). Résultat :
label == cible, navigation véridique, `check_docs` toujours vert (les cibles existent).

**Vérif.** Cibles existent ✓ · re-balayage mismatch → **0** · `ci/check_docs.sh` **PASS** ·
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**.

**Portée / anti-churn.** Édité : `README.md` (3 href) + ce journal. **0 artefact reconstruit**,
**0 gate ajouté** (#5 — origine isolée, pas une classe récurrente, cf. `confotur 8-négatifs`),
**0 chiffre inventé** (#6), **0 VPS** (#8). Structure du gate inchangée (33 checks).

## Session `20260806_051453` — VÉRIF indépendante · NOUVELLE famille objective « portabilité runtime / intégrité d'encodage » (3 axes neufs) → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (Sprints 1→7 côté
repo ; le reste — déploiement prod, builds EAS, dépôts ONAPI — est **hors périmètre worker**,
VPS `153.75.250.214` · `CLAUDE.md` #8). Les 3 derniers commits sont vérification/currency-only
(`12ec9d0` MANIFEST+dup-keys, `4bc9819` slugs, `b274958` currency). **Le canal stakeholder est
déjà courant** : la dernière **prod** est `48276ee` (FIX financement), attesté par le rapport
`daily_reports/2026-08-06.md` §034434 ; les 3 commits suivants sont non-opposables (0 édition
de prod). Re-porter la currency ou refaire un sweep déjà couvert serait du churn.

**Pré-vérif — aucun travail incomplet.** Grep `TODO|FIXME|XXX|stub|NotImplemented|à compléter`
sur tout `.py` de prod (hors `tests/`) → **0** marqueur réel ; tous les hits « placeholder »
sont le pattern #6 zéro-invention **intentionnel** (`{{…}}` documenté). Le code de prod est
complet. Le registre `OPEN_DECISIONS_REGISTER.md` (D-01→D-05) reste des **arbitrages produit**
Michel / dépendances hors dépôt, non-actionnables worker (ne pas re-surfacer, #5).

**Axes ungatés cartographiés avant de chasser.** `check_docs` gate **tous** les liens md relatifs
(`git ls-files '*.md'`, code-spans neutralisés, ancres strippées) → liens couverts ; l'unique
ancre intra-doc (`README.md:20` → `## Directives Michel (input-specs datés)`) **résout** — 1 seule
occurrence, pas de gate (#5). Reste **non couvert par aucun gate** : l'intégrité **d'octets**
des fichiers texte (encodage, BOM, fins de ligne) et la **compilabilité** des `.py` non testés.
Le CI se soucie déjà de l'encodage runtime (`PYTHONUTF8=1`, cf. en-tête `ci.yml`) mais ne
**vérifie** rien côté fichiers commités. D'où cette famille neuve, absente de la mémoire
(hash-seed / locale-TZ / forward-compat testent le *rebuild*, pas les *octets sources*).

**Axe 1 — intégrité d'encodage des fichiers texte commités.** Parse binaire des **379** fichiers
texte commités (`.json/.md/.py/.sh/.yml/.yaml/.txt/.cfg/.toml`) :
- **BOM UTF-8** (`\xef\xbb\xbf` en tête, casse `json.load` strict / rend `"﻿"+clé`) → **0**.
- **UTF-8 invalide** (`bytes.decode('utf-8')` lève) → **0**.
- **Caractères de contrôle parasites** (`< 0x20` hors `\t\n\r`, + `U+2028/2029`, + surrogates
  `D800–DFFF`) → **0**.
- **JSON non-parseable** (`json.loads` sur les **114** `.json`) → **0**.

**Axe 2 — fins de ligne (portabilité cross-plateforme).** Un `CR` dans un fichier commité
introduit une dérive d'octets invisible (diff byte / `check_artifacts` d'un runner à `autocrlf`).
Balayage `\r` sur tous les fichiers texte → **0 CRLF** (LF pur partout).

**Axe 3 — compilabilité de tout `.py` commité (dents réelles).** `python3 -m py_compile` sur les
**144** `.py` versionnés. Contrairement aux 25 suites unittest (qui n'importent que les modules
qu'elles exercent) et à forward-compat (`-W error` sur les suites), ceci prouve que **chaque**
fichier — y compris un helper `*lib/` jamais importé par un test — est syntaxiquement sain : une
erreur de syntaxe latente dans un chemin non couvert passerait **verte** aujourd'hui. → **0 erreur**.

**Verdict.** Repo **CLEAN** sur les 3 axes de la famille « portabilité runtime / intégrité
d'encodage ». **Aucun défaut de production.** Ces axes sont **réellement gardés par aucun gate**
(teeth réelles : un BOM, un octet non-UTF-8, un CRLF ou une syntaxe cassée serait détecté ici et
nulle part ailleurs), mais l'occurrence-zéro observée ne justifie **pas** un gate permanent (#5) :
consigné comme sweep **rejouable** (mémoire `runtime-encoding-integrity-sweep`), pas comme job CI.

**Portée / anti-churn.** **Production éditée : néant.** Seul ce journal touché. **0 artefact
reconstruit**, **0 gate ajouté** (#5), **0 chiffre inventé** (#6 — 379/114/144 tous recomputés
par balayage), **0 commande VPS** (#8). Structure du gate **inchangée** : `run_ci.sh` = 33 PASS
= 8 statiques + 25 suites, aucun jalon n'ajoute de job.

## Session `20260806_044449` — VÉRIF indépendante · NOUVEL axe objectif « re-dérivation des champs-compte MANIFEST depuis le payload réel » + clés JSON dupliquées → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée. Les 3 derniers
commits sont vérification-only (`4bc9819` intégrité slugs, `b274958` currency, `243523c` multi-axes).
Pour éviter le churn, **chasse à un vrai défaut ungaté sur deux axes objectifs réellement neufs**,
non couverts par les sweeps passés (mémoire : hash-seed, locale/TZ, forward-compat, intégrité
référentielle rôle/portail = *résolution de slugs*, bijection inverse, docstring↔code, liens md).

**Axe 1 — clés JSON dupliquées (classe neuve).** `json.load` garde silencieusement la **dernière**
valeur sur clé dupliquée ; la byte-repro est **aveugle** à ce défaut sémantique. Parse strict des
**114** fichiers JSON commités avec `object_pairs_hook` levant sur doublon → **0 clé dupliquée**,
0 erreur de parse. CLEAN.

**Axe 2 — re-dérivation des champs-compte MANIFEST depuis le payload réel (classe neuve).**
Distincte de « artifact internal set-semantics » (un dup ponctuel, 48276ee) et de « intégrité
référentielle slugs » (4bc9819, qui teste la *résolution* de chaque slug, **pas** la
*re-dérivation* d'un compte). Ici : chaque `nb_*`/`_total` d'un MANIFEST recomputé depuis la
liste réelle du **fichier-payload voisin**. Un bug générateur (comme le dup financement) serait
silencieux à la byte-repro mais tomberait ici. Résultats :
- **frontend/portails** — `nb_liens`/`nb_cartes`/`nb_roles` par workspace (5×3) + agrégats
  (`liens_total`=53, `raccourcis_total`=20) recomputés de `workspace.json` → **0 écart**.
  ⚠️ Faux positif initial écarté : `nb_liens` ≠ `len(links)` car le tableau ERPNext `links`
  mélange lignes `type=='Card Break'` (= cartes) **et** `type=='Link'` (= vrais liens) ;
  `nb_liens`=Σ(type==Link), `nb_cartes`=Σ(type==Card Break) — **par design**, écart initial
  (15−4=11…) = exactement le nb de cartes ; confirmé sur le champ `type`. `link_count` de
  chaque Card Break = nb de Links sous la carte.
- **frontend/chat_otoia** — `nb_roles`/`nb_knowledge_doctypes` (5×2) vs `chat_mount.json`
  (`roles_allowed`/`knowledge_scope`) → **0 écart**.
- **mobile/app_config** — `nb_roles` (5) vs `role_navigation.json` (`roles_allowed`) → **0 écart**.
- **rbac/roleprofile_gen** — `nb_roles` (6, dont Plateforme=6) vs `role_profile.json`
  (`roles`, matché par `role_profile`) → **0 écart**.
- **qa/audit_5d** — `controls_total`=17 vs Σ(`dimensions[].controls_total`)=4+3+3+4+3 → **0 écart**
  (les dimensions ne portent pas de liste de contrôles ; le sous-compte est l'autorité, gaté par
  la suite du module).

**Verdict.** Repo **CLEAN** sur les deux axes neufs. **Aucun défaut de production.** Le seul
« mismatch » apparent (nb_liens) était une convention ERPNext Card-Break/Link que mon `len()`
naïf lisait mal — re-dérivé correctement = concordance totale.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**,
**0 gate ajouté** (#5 — ces re-dérivations sont couvertes en substance par les suites de module ;
je ne fige pas un énième gate pour un axe qui sort CLEAN), **0 chiffre saisi à la main** (#6 —
tout recomputé depuis les payloads et `run_ci.sh`), **aucune commande VPS** (#8). Seule édition :
ce journal `05_activity_log/`.

---

## Session `20260806_041444` — VÉRIF indépendante · NOUVEL axe objectif « intégrité référentielle des slugs de rôles & portails inter-artefacts » → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (8 gates statiques
+ 25 suites, 24 modules dans ≥1 fiche). Les commits récents (`48276ee` dup `roles_rbac_utilises`,
`b274958` currency) ayant épuisé les axes docstring↔code / numériques / currency, et l'anti-churn
proscrivant une énième couche de currency pour des commits sans changement d'état, j'ai chassé un
**vrai défaut ungaté sur un axe OBJECTIF encore jamais balayé** : la cohérence référentielle des
identifiants de rôle et de portail cités à travers **tous** les artefacts `out/*.json` vs
l'ensemble canonique `rbac/rbac_50_roles.json` (même famille que le fix `48276ee`, mais côté
*existence des slugs* et non *unicité*).

**Axe 0 — sanity docstring↔code (modules non couverts au dernier sweep).** `Explore` sceptique sur
commissions/dossier_vente/acceptance/audit_5d/regression/deploy_runbook/ecf/portails/faisabilite/
publiciste/rbac(apply,userperm,roleprofile) : **CLEAN**. Seul « défaut » remonté = le
`validate [-o OUT]` de deploy_runbook, **non-défaut connu** (bijection compat docstring↔argparse
self-consistante — mémoire `docstring-vs-code-drift`), écarté.

**Axe 1 — rôles.** Extraction de **980** valeurs-feuilles sous toute clé matchant `/role/i` dans
les **~50 artefacts** `05_deliverables_mvp/**/out/*.json`, filtrées aux tokens slug-like kebab
(le format des 50 ids canoniques, ex. `ventes-conseiller`). **0** slug non résolu vs les 50 ids.
→ intégrité totale des références de rôle.

**Axe 2 — portails.** **66** valeurs-feuilles sous toute clé matchant `/portail|portal/i`. Les 6
tokens hors `portails_business` (5) sont tous des **non-défauts vérifiés** :
- `sell·project·buying·accounting·organization` = valeurs du sous-champ **`icon`** des objets
  portail de `mobile/app_config` (noms d'icônes ERPNext, pas le champ `portail` — lequel ne porte
  que les 5 métier canoniques) ;
- `plateforme` = le **6ᵉ portail technique légitime** (`roleprofile_gen` reporte `portails_metier`
  =5 / `portails_techniques`=1). `rbac_50_roles.json` distribue bien les 50 rôles sur **6** portails
  (`ventes`12·`construction`10·`direction`9·`compta`8·`plateforme`6·`achat`5) ; `portails_business`
  n'énumère **délibérément** que les 5 métier. Aucun `portail` réel hors des 6 attendus.

**Verdict.** Repo **CLEAN** sur le nouvel axe (rôles + portails référentiellement intègres) ; les
axes docstring↔code et numériques stakeholder restent concordants. **Aucun défaut de production.**

**Anti-gate (#5).** Pas de nouveau gate : beaucoup de ces slugs sont déjà transitivement
mono-sourcés (mémoire `roles-couverts-transitively-single-sourced`) et l'axe est une **vérification
rejouable** (à l'image de hash-seed / locale-TZ / forward-compat), pas un contrat à figer. Le
one-liner Python du sweep est consigné ici pour rejeu.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**,
**0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `980`/`66`/`50`/`6` recomputés des
artefacts + `rbac_50_roles.json`), **aucune commande VPS** (#8). `./run_ci.sh` = **33 PASS · 0 FAIL
· 0 SKIP** (inchangé, aucune édition de code). Seule édition : ce journal + une entrée mémoire.

## Session `20260806_034434` — CURRENCY canal stakeholder `a8342e9`→HEAD `48276ee` + re-vérif indépendante de la dernière prod (`48276ee`)

**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 (spec
`CHOISIR_MON_UNITE` = frontend VPS hors-périmètre #8). Aucune tâche fonctionnelle
in-repo. Constat : le daily report `2026-08-06.md` (rédigé session `011419`, attestait
HEAD `a8342e9`) ne consignait pas **deux** jalons de production 08-06 postérieurs —
`354a687` (fix help-text argparse `seo`) et surtout `48276ee` (fix générateur
`financement_bancaire` + MANIFEST reconstruit). Mémoire `two-logging-channels` : garder
le canal stakeholder courant, chaque chiffre sourcé d'artefact.

**Re-vérification indépendante de la dernière prod `48276ee` (avant de consigner).**
- MANIFEST courant `roles_rbac_utilises` = **3 objets distincts** (compta-tresorier,
  ventes-conseiller, ventes-directeur), 0 doublon → fix effectif et correct.
- Balayage surfaces sœurs (README/finlib/tests) : **aucune** ne cite l'ancien compte
  « 4 » ni une liste 4-entrées ; commentaire `builder.py:89-95` décrit correctement le
  dédoublonnage → **0 dérive résiduelle** (motif « twin fix sweep ALL surfaces », propre).
- Champ appropriément ungaté (#5) : depuis l'état propre, `check_artifacts` MORD une
  régression future (régénéré-avec-doublon ≠ committé-3-distinct → RED) → pas de gate N+1.

**Action.** Ajout d'une section de currency à `daily_reports/2026-08-06.md` : tableau
des 2 jalons de prod postérieurs (nature + opposabilité au merge), re-vérif de `48276ee`,
état courant re-dérivé au HEAD `48276ee` (8 statiques + 25 suites = 33 PASS · régression
624/607/17-skip = PASS). Chiffres recomputés de `git ls-files`/`run_ci.sh`/`ci.yml`/
`regression_run.json`, jamais saisis à la main.

**Verdict.** Dernière prod (`48276ee`) **correcte + complète**, 0 dérive résiduelle.
Canal stakeholder **à jour au HEAD**. **Aucun défaut de production.**

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**,
**0 gate ajouté** (#5), **0 chiffre inventé** (#6), **aucune commande VPS** (#8). Seules
éditions : `daily_reports/2026-08-06.md` + ce journal `05_activity_log/`.

## Session `20260806_031432` — FIX générateur (NOUVELLE sous-classe : liste per-slot vs ensemble distinct) · `crm/financement_bancaire` : `MANIFEST.roles_rbac_utilises` répétait `ventes-conseiller` (dup ungaté)

**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 5 derniers commits étant FIX-help / currency / vérif-only,
chasse **adversariale à un vrai défaut ungaté** sur un **axe frais** (anti-churn) : la
**cohérence sémantique interne des artefacts commités** (un champ « ensemble » contenant un
doublon) — axe non balayé par les 7 axes de `024430`.

**Défaut trouvé (réel, haute confiance).**
`05_deliverables_mvp/crm/financement_bancaire/out/MANIFEST.json` · `roles_rbac_utilises` portait
**4 entrées pour 3 rôles distincts** : `ventes-conseiller` / `OTO Ventes Conseiller` **répété**
(positions 1 et 4). Reproductible depuis le générateur (donc **pas** un artefact stale — byte-repro
vert) : `finlib/builder.py:89` faisait `[roles_resolved[k] for k in sorted(roles_resolved)]`,
itérant les **4 slots logiques** de `spec["roles"]` — or **deux** slots (`conseiller_wag` ET
`validation_dossier`) pointent le même rôle `ventes-conseiller` → doublon.

**Preuve que c'est un bug et non un per-slot voulu.**
1. La sortie **drope la clé de slot** (`cle`) : chaque entrée ne porte que `role_id` +
   `erpnext_role_name`. Un doublon ne transporte donc **aucune** information distinctive — si le
   per-slot était voulu, la clé de slot serait sérialisée. Smoking gun.
2. Sémantique documentée : README `MANIFEST.json → … rôles RBAC **utilisés** » (README.md:54) +
   `ci/README.md:1100` « l'**ensemble distinct** `(role_id, nom)` réellement payé ».
3. **Convention codebase** : les 3 modules frères produisent tous un ensemble **distinct**
   (`confotur` 3/3 · `commissions` 4/4 · `workflow_vente` 7/7, `n == distinct`). `financement`
   était **le seul outlier** (4/3).

**Fix (`finlib/builder.py:89`, aligné idiome `commissions`).** Ensemble **distinct** trié par
`role_id` : `[{"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)} for rid in
sorted({rr["role_id"] for rr in roles_resolved.values()})]` + commentaire expliquant le
dédoublonnage multi-slots. Artefact régénéré (`build`) → `roles_rbac_utilises` = **3/3 distinct**
(`compta-tresorier`, `ventes-conseiller`, `ventes-directeur`).

**Pourquoi 0 gate ajouté (#5).** Le champ `roles_rbac_utilises` de `financement` n'est lu par
**aucun** gate (les blocs `check_readme_claims` qui le lisent visent confotur/commissions/
workflow_vente, en **SET** — donc dedup) ni aucun consommateur externe (`grep` = 0). Le champ
`counts` n'inclut pas de compte de rôles pour ce module (contrairement aux frères) → **aucune**
prose/oracle à réaligner. Le fix corrige la **source** ; un gate distinctness serait redondant
avec la convention déjà tenue par les frères et non-régressante ici (verify-before-gate).

**Non-régression.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** après (byte-repro
`check_artifacts` **vert avec le nouvel artefact** ; les **35** tests `crm-financement-bancaire`
verts — aucun ne figeait `roles_rbac_utilises`, l.204 teste `role_validation`, non affecté).
**2 fichiers édités** (`finlib/builder.py` + `out/MANIFEST.json` régénéré), **0 chiffre inventé**
(#6 — 4/3 recomputés), **aucune commande VPS** (#8).

## Session `20260806_024430` — VÉRIF indépendante multi-axes (7 axes) → **CLEAN · 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 côté dépôt (les restes roadmap = livrables VPS/live hors
périmètre worker · #8/instructions). Les 5 derniers commits étant FIX-help / currency / prose,
chasse **adversariale ciblée à un vrai défaut ungaté** sur des **axes frais** (anti-churn : ne PAS
réempiler une énième couche help-text/currency/prose déjà balayée par `021424`/`014423`/`011419`).
Chaque axe est soit corrigé, soit prouvé accurate/gaté — **aucune édition manufacturée** (#5/#6).

**Les 7 axes balayés (tous CLEAN) :**

1. **README « Entrée CLI » ↔ argparse** (23 modules) — sous-commandes / flags documentés dans les
   README vs `add_parser(...)`/`add_argument(...)` réels. Aucun flag/sous-commande fantôme ni
   divergence de nom (`--out` vs `--output`, etc.). Distinct de l'axe help-TEXT (déjà clos `021424`).

2. **Sémantique du CLI `validate`** — `cmd_validate` **re-construit en mémoire** (`_build`) puis
   valide ; il ne relit **pas** l'artefact `out/` commité. **By-design, PAS un défaut de dents** :
   la dérive byte d'un `out/` corrompu est couverte par `check_artifacts` (diff byte-à-byte).
   Noté pour qu'une session future ne le « flag » pas comme teeth manquantes (verify-before-flag).

3. **Ledger 15 invariants SEO** — `seo_gen.py` numérote bien **1…15** (`# N ·`, l.77-215) ; les
   3 surfaces prose (`validate` succès l.285, help `validate` l.297, README `## Garanties (15
   invariants)`) disent **15**, self-consistent. Le « =9 » de la mémoire `invariant-ledger-count`
   = **9 MODULES** en dialecte A, PAS 9 invariants/module (relecture — non-contradiction).

4. **Root `README.md` « 24/24 modules · 24 suites »** — correctement **sourcé** : `24 modules` =
   périmètre `audit_4big/out/quality_report.json` ; `24 suites` = `regression_plan.json` qui
   **s'auto-exclut** (`self_module: qa/regression`, ISA 315 · séparation des pouvoirs) → 24 =
   25 gates-de-test `run_ci` − 1 (le harnais régression lui-même). **Piège de réconciliation**
   24-vs-25 : NON-défaut. `totals.suites=24 · test_methods=624`, cohérent avec `regression_run.json`.

5. **Prose `notes[]` embarquée dans les artefacts JSON** (`regression_plan.json`, 5 notes) —
   surface libre re-générée+byte-gatée ; contenu accurate (dérivation `.gitea/workflows/ci.yml`
   via `q4lib/registry.parse_ci`, self-exclusion, anti-invention #6). CLEAN.

6. **Complétude de `gate.needs`** — le job d'agrégat `gate` (seul verrou de merge) liste bien les
   **33** checks (8 statiques + 25 suites) ; `e2e-baseline` correctement exclu (manuel
   `workflow_dispatch`). **Déjà GATÉ** par `ci/check_ci_integrity.sh` **INV-A**
   (`gate.needs == {tous jobs} − {gate} − {manuels}`) → une dérive de câblage casserait le gate.

7. **Commentaires inline `#` cross-fait/compteur** (dans `*_gen.py` + `*lib/`, PAS docstrings/README
   déjà audités) — 14 commentaires porteurs de compte vérifiés vs la réalité qu'ils décrivent
   (50 rôles, 3 langues, 5 portails, 4 capacités OTOIA, 12 groupes Master Data PIE, 5 dimensions
   audit_5d, ≥2 modules démo, axe5=20 pts, 4 conditions financement…). Tous synchronisés. CLEAN.

**Non-régression.** **0 fichier de production touché** (`git status` vide avant/après). `run_ci.sh`
**33 PASS · 0 FAIL · 0 SKIP** inchangé. 0 artefact reconstruit, 0 gate ajouté (#5 — tout défendable
est déjà gaté), 0 chiffre inventé (#6 — tout recomputé depuis artefacts/code), 0 VPS (#8).

## Session `20260806_021424` — FIX argparse-help↔code (classe `docstring-vs-code-drift`, sous-classe help-text du fix `a8342e9`) · `seo/seo_gen.py` : le help `build` nommait des fichiers de sortie INEXISTANTS

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
Les 5 derniers commits étant currency/vérif-only, chasse **adversariale à un vrai défaut ungaté**,
en poursuivant la sous-classe ouverte par le fix `a8342e9` (session `004414`) : le **texte help
d'une sous-commande argparse énumérant ses fichiers de sortie**. Audit exhaustif des **22
générateurs** (`*_gen.py` avec sous-commande `build`/`generate`) : annoncé-dans-le-help ↔
réellement-écrit-par-le-code.

**Résultat de l'audit.** 21/22 CLEAN — mais **1 vrai défaut** trouvé, haute confiance :
`05_deliverables_mvp/seo/seo_gen.py:293`. Le help `build` disait
`"genere seo_keywords/schema_org/hreflang + MANIFEST"`, or les fichiers RÉELLEMENT écrits
(`seo_gen.py:259-262`) sont `seo_keywords.json`, **`seo_schema_org.json`**, **`seo_hreflang.json`**,
`MANIFEST.json`. Le help **droppait le préfixe `seo_`** sur 2 des 3 fichiers (`schema_org`,
`hreflang`) ET l'extension `.json` sur tous : il nommait donc des fichiers **qui n'existent pas**
(`schema_org` ≠ `seo_schema_org.json`). Ce n'est pas un simple raccourci toléré — exactement la
classe du fix `a8342e9` (le help ment sur la surface de sortie que l'agent lit pour lancer son
propre livrable).

**Preuve que c'était bien la SEULE surface fautive (fix en une passe).** Le docstring module
(`seo_gen.py:6-9`) ET le README (`README.md:15-18`) nomment déjà correctement les 4 fichiers
exacts (`out/seo_schema_org.json`, `out/seo_hreflang.json`, …) — c'est le help l'outlier, pas
l'inverse. Les modules frères (`commissions`, `audit_5d`, `app_config` post-`a8342e9`) énumèrent
des **noms de fichiers complets** ; `seo` était le seul en raccourci imprécis.

**Fix (`seo_gen.py:293`).** Help → `"genere seo_keywords.json / seo_schema_org.json /
seo_hreflang.json / MANIFEST.json"` — exact, aligné sur le docstring/README du module ET la
convention des frères. Style respecté : ASCII sans-accent (`genere`, comme le reste du fichier).

**Pourquoi 0 gate ajouté (#5).** Le help n'est ni sérialisé dans un artefact ni parsé par un gate
(le gate `check_readme_claims` « Entrée CLI » ne lit que le NOM `add_parser("build")` via AST, pas
le `help=`). Un gate parseur du help serait **fragile** : les 22 help strings n'ont PAS de
convention machine-parsable commune (`seo` en raccourci, `faisabilite` décrit un répertoire
`data_room/PXX/`, séparateurs `/` vs `+` mêlés). Mauvais compromis robustesse/valeur → la classe
reste couverte par l'audit périodique (ici 22/22 balayés), pas par un gate.

**Non-régression.** Sortie `build` **byte-identical** aux 4 artefacts `out/` commités (help = code
pur, non sérialisé → 0 rebuild). `python3 -S seo_gen.py build/validate` OK. `run_ci.sh` **33 PASS ·
0 FAIL · 0 SKIP**. 0 prod (#8), 0 artefact reconstruit, 0 chiffre inventé (#6 — tout recomputé).

## Session `20260806_014423` — FIX prose-numérique canal stakeholder · le compte « bijection README↔réel » de la session `011419` sous-comptait (14 → **16**) · 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. Les 5 derniers commits étant currency/vérif-only, chasse
**adversariale ciblée à un vrai défaut ungaté** — pas une énième couche de currency (anti-churn).
Premier axe (comment↔code des `ci/*.sh`, classe du fix `8903a4b`) → un candidat `check_regression.sh:17`
**écarté comme faux positif** : le commentaire « (ran/passed/failures) » décrit ce que l'**artefact**
*porte* (le JSON contient bien `failures:0`), pas la ligne d'affichage `l.79` ; le contrat réel du
gate est le `diff -q` byte-à-byte du fichier entier (`l.71`), qui couvre `failures`. Non-défaut.

**Défaut trouvé (réel, haute confiance) — classe `prose-facts-vs-numeric-drift`.** L'« axe de
vérification objectif » introduit par la session `011419` (bijection *compte de tests annoncé dans
chaque README* ↔ *nombre réel de `def test_*`*) affirmait **« concordance totale sur les 14 READMEs
annonceurs »** — figure reprise à l'**identique dans DEUX surfaces** : le canal stakeholder
`daily_reports/2026-08-06.md` (l.48-54) **et** ce journal (entrée `011419`). Or l'énumération de
`011419` liste effectivement 14 modules mais **en omet 2** qui annoncent pourtant un compte dans
leur README, tous deux concordants avec le réel : **`frontend/portails 19`** (19 `def test_`) et
**`demo/scenarios 39`** (39 `def test_`). Le vrai total est **16 annonceurs / 9 sans annonce**,
**corroboré par l'en-tête faisant autorité** de `ci/check_readme_claims.sh` (l.96 : « **27 claims
sur 16 modules** »). Le « 11 sans annonce » du canal était le complément erroné (25 − 14) → **9**.

**Fix (sweep des 2 surfaces en une passe, cf. `prose-facts-vs-numeric-drift`).**
- `daily_reports/2026-08-06.md` : `14 → 16`, ajout des 2 entrées manquantes à l'énumération,
  `11 → 9` sans annonce, + ancrage explicite à l'en-tête « 16 modules » du gate.
- ce journal : `14 → 16` dans l'entrée `011419` (fait présent sur l'état courant du repo) avec
  commentaire de traçabilité ; nouvelle entrée (celle-ci) documentant la correction.
Chiffres re-dérivés, non saisis : `grep -rlE '[0-9]+ tests' README.md` = **16** annonceurs · `def test_`
par suite = concordance **16/16** · `find -name tests` = **25** suites → complément **9** · en-tête gate = **16**.

**Non-régression.** Le compte est un fait de prose **ungaté** (canal stakeholder + journal, classe
`two-logging-channels` : chiffre à sourcer d'artefact, ici recomputé). Aucun oracle/gate ne fige
cette phrase (vérifié) : édition sûre. `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** avant/après.

**Portée.** **0 fichier de production édité** (2 docs : 1 deliverable stakeholder + ce journal),
**0 artefact reconstruit**, **0 gate ajouté** (#5 — correction de fait, pas un contrôle ; la surface
de compte-de-tests des READMEs est déjà gatée par `check_readme_claims.sh`), **0 chiffre inventé**
(#6 — `14→16`/`9`/`25` recomputés), **aucune commande VPS** (#8).

## Session `20260806_011419` — CURRENCY canal stakeholder porté au HEAD `a8342e9` + axe de vérif objectif ajouté (bijection compte-de-tests README↔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. Le canal stakeholder `daily_reports/` s'arrêtait au
**2026-08-05** (HEAD `8903a4b`) alors que **deux** jalons du **2026-08-06** avaient depuis été
livrés — dont un **vrai fix de production** (`a8342e9`, help argparse `mobile/app_config` omettant
`store_listing.json`) **non encore consigné** côté stakeholder. Contrairement aux currency
purement vérification-only (motif anti-churn adopté en `234409`/`231404`), il y a ici une **vraie
évolution d'état** à reporter → currency légitime du canal lapsé (`two-logging-channels`).

**Ce qui a été fait.** Création de `05_deliverables_mvp/daily_reports/2026-08-06.md` :
- tableau des **2 jalons 08-06** (`fe61207` VÉRIF-only · `a8342e9` FIX 1-ligne help-text) avec
  nature + opposabilité au merge ;
- état courant re-dérivé d'artefacts commités : **33 PASS** (8 statiques `git ls-files ci/*.sh`
  hors `lib.sh` + 25 suites) · régression `624 exécutés / 607 passés / 17 skippés / 0 échec = PASS`
  (`qa/regression/out/regression_run.json` `totals`, 17 skips par design) ;
- **axe de vérification objectif ajouté ce jour** : bijection `N tests` annoncé dans chaque
  README ↔ nombre réel de `def test_*` → **concordance totale sur les 16 READMEs porteurs d'une
  annonce** (seo 36 · ecf 39 · confotur 44 · dossier_vente 31 …), 0 dérive.
  <!-- CORRIGÉ session 014423 : « 14 » était un sous-comptage — l'énumération de ce jour
  omettait `frontend/portails 19` et `demo/scenarios 39` ; le vrai total est 16, concordant
  avec l'en-tête faisant autorité « 27 claims sur 16 modules » de ci/check_readme_claims.sh. -->

**Vérif indépendante préalable (lecture-seule).** docstring↔code re-vérifié sur modules non
balayés la veille : `publiciste` (docstring/help/writes cohérents = `projets_master.json` +
`index.html`) · `fiscal/ecf_dgii` (« 12 invariants » = schéma(1)+2→12 numérotés, self-consistant,
**pas** un défaut). Couverture CI de `publiciste` confirmée dans `gate.needs`. Structure
`ci/*.sh`↔`gate.needs` intacte (8 statiques).

**Non-régression.** Le rapport est une doc stakeholder pure : `./run_ci.sh` = **33 PASS · 0 FAIL
· 0 SKIP** avant et après.

**Portée.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate ajouté**
(#5 — currency documentaire, pas un contrôle), **0 chiffre inventé** (#6 — 8/33/25/624/607/17
recomputés de `git ls-files` · `run_ci.sh` · `.gitea/workflows/ci.yml` · `regression_run.json`),
**aucune commande VPS** (#8). Seules éditions : le rapport de currency + ce journal.

## Session `20260806_004414` — FIX docstring↔code (sous-classe help-text argparse) · `mobile/app_config_gen.py` : le help de la sous-commande `build` omettait `store_listing.json`

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, roadmap
intégralement livrée/gatée. Plutôt qu'une énième passe vérification-only (les 5 derniers
commits l'étaient), **chasse adversariale ciblée à un vrai défaut ungaté** sur la classe
RECURRING `docstring↔code`, scopée aux modules **non** balayés la veille (session `001412`
avait couvert seo/financement/demo/pie/rbac/audit_4big/chat/workspaces/workflow_vente/confotur).

**Défaut trouvé (réel, haute confiance).** `05_deliverables_mvp/mobile/app_config/app_config_gen.py:279` —
le `help=` de `sub.add_parser("build", …)` énumérait **4** sorties
(`app_config.json / eas_build.json / role_navigation.json / MANIFEST.json`) alors que
`cmd_build()` en écrit **5** (l.240-244) : `store_listing.json` (l.243) était **omis** du help.
La docstring (l.15, « écrit les 5 fichiers ci-dessus ») et le README (l.56/68) listent bien les 5.
Nouvelle **sous-classe help-text argparse** du motif `docstring↔code` — distincte des sorties
`open(...,'w')` vs docstring déjà balayées : le help CLI est une 2ᵉ surface prose dans le code.

**Fix.** Ajout de `store_listing.json` dans le `help=`, à sa position canonique
(…role_navigation → **store_listing** → MANIFEST), ordre identique docstring/README/code.
Aucun gate/test ne fige cette chaîne (`grep -rn` = 0 hit hors le fichier) → édition sûre,
pas d'oracle à réaligner.

**Non-régression.** Le help n'affecte aucune sortie : artefacts **byte-identiques**
(`ci/check_artifacts.sh` vert dans le merge). `--help` rend désormais les 5 fichiers.
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** avant et après.

**Portée.** **1 fichier de production édité** (1 ligne, help argparse), **0 artefact reconstruit**
(sortie inchangée), **0 gate ajouté** (#5 — la chaîne n'était figée par personne, on corrige
la source pas de nouveau gate), **0 chiffre inventé** (#6), **aucune commande VPS** (#8).
Reste de l'audit (audit_5d/acceptance/regression/deploy_runbook/commissions/dossier_vente/
ecf/portails/faisabilite) = **CLEAN**, comptes d'invariants et noms de sorties concordants.

## Session `20260806_001412` — VÉRIF indépendante multi-axes (docstring↔code · numériques du canal stakeholder · liens in-repo) → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre.
La roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` est intégralement livrée/gatée (8 gates
statiques + 25 suites, 22 modules dans ≥1 fiche). Le canal de currency
`daily_reports/2026-08-05.md` attestait au HEAD `8903a4b` ; depuis, **deux** commits
**vérification-only** (`21f55a9` currency, `c196e41` VÉRIF adversariale, tous deux « 0 prod »).
Conformément à la décision anti-churn déjà prise en `234409` (« plutôt que répéter le motif de
currency »), **pas** de énième couche de currency pour deux commits sans changement d'état
stakeholder. À la place : **chasse indépendante à un vrai défaut ungaté** sur trois surfaces,
plus re-dérivation des chiffres du canal stakeholder pour prouver qu'aucun n'a dérivé.

**Axe 1 — docstring↔code (classe RECURRING).** Balayage `Explore` skeptique des générateurs de
production + helpers, sortie-par-sortie : noms de fichiers `open(...,'w')` vs docstring/README,
flags `argparse` vs docstring, comptes d'invariants du ledger numéroté vs docstring. **13 modules
audités CLEAN** (seo 4/4 fichiers · financement_bancaire 7/7 · demo_scenario 3/3 · pie 2/2 ·
rbac_fixtures 3/3 · audit_4big 2/2 · chat_otoia 3/3 · workspaces 2/2 · workflow_vente 4/4 ·
confotur 2/2 …). Comptes d'invariants concordants (seo 15 · audit_5d 15 · pie 10 · rbac_apply 7 ·
workflow_vente 9 · confotur 14 · dossier_vente 12 · commissions 10 · financement 12). Non-défaut
connu ré-écarté : `validate [-o OUT]` = bijection compat 8-vs-13 self-consistante, **pas** un défaut.

**Axe 2 — numériques du canal stakeholder re-dérivés au HEAD.** Aucune dérive :
- SEO **258** mots-clés recomputés de `seo/out/seo_keywords.json` → **fr=87 · en=87 · es=84** (concorde).
- Gates statiques : `git ls-files 'ci/*.sh'` hors `lib.sh` = **8** (concorde).
- Suite de merge : `run_ci.sh` = **33** PASS (= 8 statiques + 25 suites, concorde).

**Axe 3 — liens markdown in-repo (surface objective).** Extraction de tous les `[texte](cible)`
repo-wide non-`http`/`#`/`mailto` : **294** liens, **291 résolvent**. Les **3** « cassés »
sont des **faux positifs** — des exemples de *syntaxe* de lien cités en prose dans des journaux
**historiques**, non des liens réels : `2026-08-02.md:392` (`[texte](cible)` documentant le
comportement de `check_docs.sh`) et `2026-07-30.md:1432/1477/1479` (l'exemple pédagogique
`…/README.md` **à ellipse** que le journal lui-même décrit comme « déclaré cassé par la regex »).
Le vrai gate `check_docs.sh` **neutralise** les code-spans à backticks et ne les compte pas ;
ma regex ad-hoc, plus grossière, a simplement reproduit le faux positif que le journal explique.
Classe *pedagogical-hypothetical* = **KEEP** (motif prose-facts). **0 lien réel cassé.**

**Verdict.** Repo **CLEAN** sur les trois axes + numériques stakeholder intacts. **Aucun défaut de
production.** Session de vérification, à l'image de `d6a8d73`/`c196e41`.

**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`/`87`/`87`/`84`/`294`/`291`
recomputés de `git ls-files` · `run_ci.sh` · l'artefact · le sweep de liens), **aucune commande
VPS** (#8). Seule édition : ce journal `05_activity_log/`.

## Session `20260806_061455` — CURRENCY canal stakeholder porté `48276ee` → HEAD `e786c12` (prose-facts drift) · 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 (Sprints 1→7 côté repo ; déploiement prod / builds EAS /
dépôts ONAPI = hors périmètre worker, VPS `153.75.250.214`, `CLAUDE.md` #8). Les **~10 derniers
commits** sont majoritairement des sweeps vérification-only à rendement décroissant. Objectif :
éviter le churn (re-sweep déjà couvert) et corriger un **fait de prose réellement dérivé**.

**Axes ré-audités CLEAN (non re-flaggés).** (1) liens md avec ancre `#…` → **1 seule**
occurrence (`README.md:20`), résout, déjà en mémoire. (2) Robustesse shell des gates : `set -e`
absent partout mais **intentionnel** (les gates accumulent les échecs puis `exit` en fin ;
`lib.sh::cd_repo_root` couvre le « vert trompeur » hors-git). (3) Mode git + shebang des 9
`ci/*.sh` : tous `100755 #!/usr/bin/env bash` sauf `lib.sh` `100644` (sourcé, correct) ; invoqués
`bash ci/…` dans `ci.yml`. → cohérents, **0 défaut**.

**Défaut réel trouvé (classe `prose-facts-vs-numeric-drift`).** Le canal stakeholder
`05_deliverables_mvp/daily_reports/2026-08-06.md` était ancré au HEAD `48276ee` et affirmait
« dernière prod = `48276ee`, tout le reste 0-édition ». Or **5 commits** ont atterri depuis, et
l'un — `e786c12` — **édite `README.md`** (3 href, fix label↔cible). La prose « dernière prod »
dérivait donc vs le graphe de commits réel : `git log 48276ee..HEAD` + filtrage des fichiers
hors `05_activity_log/` & `daily_reports/` → **4 commits journal-only** + **1 édition dépôt réelle**
(`e786c12` → `README.md`). → **FIX** = porter la currency à HEAD `e786c12`, tabuler l'opposabilité
au merge des 5 commits, ré-attester l'état courant.

**Chiffres re-dérivés (#6, aucun saisi à la main).** `8` gates statiques (`git ls-files 'ci/*.sh'`
hors `lib.sh`) · `25` suites (`*-tests` de `gate.needs`) · `25` modules testés (`find test_*.py`) ·
`624`/`607`/`17` (`regression_run.json::totals`) · `33 PASS` (`run_ci.sh`). **Tous inchangés** vs
`48276ee` — `e786c12` ne touche que des href → aucune sortie/compte modifié, `check_artifacts`
byte-identique.

**Vérif.** `ci/check_docs.sh` **PASS** · `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**.

**Portée / anti-churn.** Édité : `daily_reports/2026-08-06.md` (section currency) + ce journal.
**0 artefact reconstruit**, **0 gate ajouté** (#5 — dérive prose isolée, pas une classe
récurrente à gater), **0 chiffre inventé** (#6), **0 commande VPS** (#8). Structure du gate
inchangée (33 checks).

## Session `20260806_064502` — Hypothèse « copie-constante à single-sourcer » RÉFUTÉE par le gate · +1 commentaire anti-footgun · 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. Les ~10 derniers commits sont des sweeps vérif-only à
rendement décroissant. Objectif : traquer une **vraie duplication de constante mandat** à
single-sourcer (classe `fix-vs-gate-transitively-protected-constant` : DELETE la copie, pas
gate N+1), pas re-sweeper du déjà-couvert.

**Piste investiguée (constantes canoniques #9/#10).** Les 6 marqueurs `3 % / 8.5 % / 52 % /
USD + DOP / Cardnet / Letter US` existent en **≥4 endroits** : (a) `CLAUDE.md` #9/#10 = source ·
(b) `genlib/model.py` CANONICAL (émis dans chaque rendu) · (c) `genlib/renderer.py`
`_CANONICAL_MARKERS` (oracle de `check_canonical`) · (d) `tests/test_generator.py` la liste
`for marker in [...]`. J'ai d'abord lu la 1ʳᵉ moitié du gate `check_readme_claims` qui n'ancrait
explicitement que (a→b) + la prose README → **hypothèse** : (c) et (d) sont des copies **non
gatées** → les dériver de `model.CANONICAL` (source unique auto-suivante).

**RÉFUTATION — le gate a mordu (bien joué la discipline `run_ci`).** Édité (c)+(d) →
`./run_ci.sh` = **32 PASS · 1 FAIL** (`check-readme-claims`). La **2ᵉ moitié** du même gate
(bloc ligne ~5475) `ast.literal_eval` **statiquement** les DEUX listes (regex
`_CANONICAL_MARKERS\s*=\s*(\[.*?\])` et `for marker in (\[...\])`) et les set-diffe vs les 6
valeurs re-dérivées de CLAUDE.md. Donc (c)/(d) étaient **déjà gatés**. Pire : la dérivation
casse la raison d'être même de l'oracle — `check_canonical()` **prouve** que le rendu (bâti
DEPUIS `model.CANONICAL`) émet bien chaque valeur ; le dériver de `CANONICAL` rend le contrôle
**tautologique** (« md-bâti-depuis-X contient X » = vert vacux). Le commentaire du gate le dit
noir sur blanc : « si cette liste DÉRIVE de CANONICAL … le contrôle NE VÉRIFIE PLUS RIEN ».

**Conclusion — EXCEPTION à `fix-vs-gate-transitively-protected-constant`.** Cette règle vise
les copies **passives** (prose, config). Un **oracle d'enforcement** est **actif** : sa
duplication EST le test ; l'ancrage correct = garder le littéral + gate statique vs la source
(exactement l'état existant). **Revert** intégral des 2 éditions (test byte-identique restauré).

**Livrable retenu.** Un seul changement survit : **+5 lignes de commentaire anti-footgun** à
`renderer.py:275`, remontant au **site de code** la raison (indépendance de l'oracle) autrement
enfouie dans le script CI — pour qu'un mainteneur (ou moi, prochaine session) ne refasse pas la
« DRY-ification » qui décapite l'oracle. Sortie **inchangée** (commentaire pur), `check_artifacts`
byte-identique. Mémoire `enforcement-oracle-must-stay-independent` créée.

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

**Portée / anti-churn.** Édité : `renderer.py` (1 commentaire) + ce journal + mémoire. **0 artefact
reconstruit**, **0 gate ajouté** (#5), **0 chiffre inventé** (#6 — les 6 marqueurs déjà présents,
re-dérivés du gate), **0 commande VPS** (#8). Structure du gate inchangée (33 checks).

---

## Session `20260806_071503` — anti-footgun in-fiche : documenter l'omission MANIFEST volontaire de la colonne « Sortie » · 0 artefact reconstruit

**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre, roadmap
intégralement livrée + gatée. HEAD `ddcafc5` (commentaire anti-footgun `renderer.py`, output
inchangé). Les ~15 derniers commits sont des sweeps vérif-only / currency à rendement décroissant
(la session précédente le notait). Objectif : **éviter d'ajouter un énième « CLEAN sweep »** et
trouver une amélioration *durable* réelle.

**Chasse au défaut (Explore adversarial).** Dispatché un agent lecture-seule sur 3 surfaces
(fiches `03_agents/*/AGENT.md` ↔ modules réels · READMEs ↔ `*_gen.py` · liens label↔cible).
Retour : **1 candidat** — `frontend_console/AGENT.md:29` « colonne Sortie du chat_otoia omet
`MANIFEST.json` » alors que `chat_otoia_gen.py:237` écrit bien les 3 fichiers.

**Réfutation via mémoire — FAUX POSITIF connu.** C'est exactement `fiche-sortie-cell-omits-
manifest-by-design` : la colonne « Sortie » liste le **payload métier** seul ; le MANIFEST est
omis **dans les DEUX rangs** (portails + chat) = convention éditoriale, PAS une dérive ; les
docstrings/READMEs énumèrent bien les 3 (byte-gatés). « Corriger » violerait #5/#6 et risquerait
de casser le gate (la cellule est regex-parsée par `check_readme_claims.sh`). **0 édition de ce
faux positif.**

**Signal actionnable.** Le fait qu'un Explore **re-flague la MÊME cellule cette session** (déjà
flaguée session 234409) = preuve que la convention est un **footgun non documenté** : elle brûle
des cycles d'audit et risque un « fix » erroné (ajout MANIFEST à la cellule gate-parsée → churn
#5, voire RED). Exactement le motif de la session précédente (commentaire anti-footgun remonté au
**site** dans `renderer.py`).

**Livrable — note anti-footgun in-fiche.** Ajouté au `frontend_console/AGENT.md` (juste après la
table + les puces Portails/Chat) un blockquote `> **Note de lecture — colonne « Sortie ».**`
expliquant que la cellule liste le payload seul, que chaque générateur écrit **en plus** un
`out/MANIFEST.json` **volontairement omis** par convention, que l'énumération exhaustive vit dans
docstrings/READMEs byte-gatés, et **« ne PAS l'ajouter »** (cellule regex-parsée). Rédigé pour
**éviter les regex ancrées** du gate (aucun motif `N cartes/M liens/K rôles`, aucun
`**N tests** · job CI`).

**Vérif.** `check_readme_claims.sh` exit 0 · `check_docs.sh` exit 0 · `./run_ci.sh` =
**33 PASS · 0 FAIL · 0 SKIP**. Mémoire `fiche-sortie-cell-omits-manifest-by-design` mise à jour
(récurrence + mitigation in-fiche consignées).

**Portée / anti-churn.** Édité : `frontend_console/AGENT.md` (1 blockquote de prose) + ce journal
+ mémoire. **0 artefact reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 —
la note ne contient aucun compteur), **0 commande VPS** (#8). Structure du gate inchangée (33 checks).

## Session `20260806_074504` — VÉRIF · NOUVEL axe méta « bijection de câblage CI » (fichiers `ci/*.sh` ↔ jobs `ci.yml` ↔ miroir `run_ci`) → CLEAN + auto-protégé · 0 édition de production

**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre, HEAD `4c741a7`,
roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (Sprints 1→7 côté repo ; prod/EAS/
ONAPI hors périmètre worker, VPS `153.75.250.214` · `CLAUDE.md` #8). Les **~15 derniers commits**
sont des sweeps vérif-only / currency à rendement décroissant.

**Currency écartée (serait du churn).** Depuis la dernière currency `daily_reports` (`e786c12`),
**2 commits** seulement : `ddcafc5` (commentaire `renderer.py`) + `4c741a7` (blockquote
`frontend_console/AGENT.md`) — **0 changement de sortie/compte** (`check_artifacts` byte-identique).
Pas de vraie évolution d'état stakeholder → re-porter la currency serait du churn (`two-logging-channels`
= currency légitime **seulement** sur changement d'état réel). **0 édition du canal.**

**Axe NEUF chassé (absent de la mémoire) — bijection de câblage CI.** Distinct de
`gate-enumeration-completeness` (compte de gates dans la prose) et `ci-gate-verification-method`
(archive vs runner) : la cohérence des **trois couches** filesystem `ci/*.sh` ↔ jobs `.gitea/workflows/ci.yml`
↔ miroir local `run_ci.sh`.
- **8/8 gates statiques invoqués** par un job `ci.yml` (`check_artifacts / check_ci_integrity /
  check_docs / check_mobile_workflow / check_readme_claims / check_regression / guard_constraints /
  validate_json`) → **aucun gate orphelin/mort**.
- **`run_ci.sh` ne peut pas dériver de la CI par construction** — il **ne code en dur aucun nom de
  script** (grep = 0 réf sur 7/8 ; l'unique hit `check_ci_integrity` est un commentaire). Il **dérive**
  la liste de `gate.needs` dans `ci.yml` et rejoue chaque `run:`/`working-directory:` déclaré, en
  **imprimant** le décompte jobs/gates/suites à l'exécution. Un gate ajouté/retiré s'y répercute
  automatiquement ; un « vert local mais rouge au push » est **impossible**. L'en-tête `run_ci.sh`
  le documente noir sur blanc, et `run_ci` lui-même n'est **pas** un gate (#5, `local-ci-runner`).
- Prose « 7/7 · 8 gates statiques verts » (`ci/README.md` · `check_mobile_workflow.sh:13` ·
  `check_readme_claims.sh:9202`) = **descriptions pédagogiques de dents de mutation** (ce qu'un retrait
  de ligne laisserait vert), **pas** des comptes d'état présent → **KEEP** (`prose-facts-vs-numeric-drift`).

**Une sous-classe réellement ungatée (occurrence-zéro ⇒ sweep, pas gate, #5).** `check_ci_integrity`
INV-A valide `gate.needs == jobs − gate − manuels` à partir de la **liste de jobs de ci.yml**, PAS de
l'ensemble **filesystem** `ci/*.sh`. Un script commité sur disque mais **jamais câblé** dans un job
serait un gate **mort silencieux** — non détecté. CLEAN aujourd'hui (8/8 câblés) ; verdict identique à
`runtime-encoding-integrity-sweep` (dents réelles, occurrence-zéro ⇒ rejouable, on ne fige **pas** un gate).

**Verdict.** Repo **CLEAN** + **auto-protégé** sur l'axe de câblage CI. **Aucun défaut de production.**

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate ajouté**
(#5), **0 chiffre saisi à la main** (#6 — `8`/8 recomputé par balayage), **0 commande VPS** (#8).
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Seules éditions : ce journal + mémoire
`ci-wiring-bijection-sweep` (+ pointeur `MEMORY.md`).

## Session `20260806_081509` — VÉRIF récurrence du bug *set-semantics* (48276ee) sur **TOUS** les artefacts → CLEAN, détecteur à dents prouvées

**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, **0 marqueur `TODO`/`FIXME`** en production (recompté). La currency du canal
stakeholder est déjà à jour (le dernier commit `def4706` l'a explicitement écartée : les commits depuis
`e786c12` sont doc/commentaire, **0 sortie changée**) → un re-port serait du churn. Plutôt qu'un énième
sweep déjà catalogué en mémoire, **chasse à un défaut RÉEL** : le dernier bug générateur commité
(`48276ee`) était un **champ « ensemble » portant un doublon silencieux** (`roles_rbac_utilises` bâti en
itérant des **slots** logiques ; 2 slots → même rôle → 4 entrées/3 distincts). Question ouverte : **ce
bug se répète-t-il ailleurs ?** Un seul champ avait été audité au moment du fix.

**Méthode — généralisation du bug à l'univers complet des artefacts.** Scan des **57 `out/*.json`**
commités : pour chaque tableau, deux formes du bug —
- **SCALAR-DUP** : tableau de scalaires (la forme EXACTE du bug `48276ee`, ex. `["a","b","a"]`) → **0 hit / 57 artefacts**.
- **OBJ-DUP** : tableau d'objets avec une clé d'identité dupliquée (`id`/`role_id`/`slug`/`doctype`/`permlevel`…) → **33 hits**, tous **BY-DESIGN** après revue :
  - `doctype` répété sur les lignes de fixtures Frappe (chaque ligne porte le même DocType) — attendu ;
  - `permlevel: 0` répété sur plusieurs lignes de permission — attendu ;
  - `role_id` répété dans `evenements`/`emission_events`/`depot_events` — ce sont des **journaux d'événements**, pas des ensembles : un workflow fait légitimement agir le **même** rôle sur plusieurs étapes.

**Contrôle ciblé du champ-ensemble d'origine.** Les **6** MANIFEST portant `roles_rbac_utilises` sont
**tous distincts** : `commissions 4/4` · `dossier_vente 7/7` · `financement_bancaire 3/3` (le fix
`48276ee` tient) · `workflow_vente 7/7` · `ecf_dgii 1/1` · `confotur 3/3`. **0 doublon résiduel.**

**Dents prouvées (une CLEAN d'un détecteur sans dents ne vaut rien).** Test de mutation en mémoire :
le détecteur SCALAR-DUP **mord** sur la forme plantée `["a","b","a"]` (= la forme `48276ee`) et **ne
lève pas de faux positif** sur `["a","b","c"]`. Le CLEAN est donc opposable.

**Verdict.** Le bug *set-semantics* (`artifact-internal-set-semantics`) **ne se reproduit nulle part**
dans les artefacts commités ; distinction clé établie et outillée : **champ-ensemble** (doit être
distinct) vs **journal-d'événements / lignes-de-fixtures** (répétition légitime). **Aucun défaut de
production.** Sweep lecture-seule, occurrence-zéro, rejouable → **pas un gate** (#5, sibling de
`role-portail-referential-integrity-sweep` et `manifest-count-field-payload-rederivation`).

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate ajouté**
(#5), **0 chiffre saisi à la main** (#6 — `57`/`6`/comptes distincts recomputés par balayage), **0
commande VPS** (#8). `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Seules éditions : ce
journal + mémoire `artifact-internal-set-semantics` (extension « no-recurrence sweep »).

## Session `20260806_084514` — VÉRIF axe neuf « dents de l'ORACLE jsonschema » (26 `*.schema.json`) → CLEAN 26/26, détecteurs à dents prouvées

**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** (8 sprints, 22 modules gatés, métriques MVP
atteintes) ; **0 marqueur `TODO`/`FIXME`/`NotImplemented`** en production (recompté — tous les hits
`placeholder` sont le motif de design #6 « jamais 0 inventé ») ; currency du canal stakeholder déjà
écartée par `def4706`/`081509` (commits depuis `e786c12` = doc/commentaire, 0 sortie changée). Plutôt
qu'un énième re-sweep déjà catalogué, **chasse d'un défaut RÉEL sur un axe non encore audité**.

**Axe neuf : les dents de la couche ORACLE elle-même.** ~20 modules embarquent un `test_schema_oracle`
qui fait `jsonschema.validate(<build en mémoire>, schema)`. Il valide le build **frais** (qui conforme
toujours par construction) contre un `*.schema.json` **commité**. Si un schéma était **vide**
(`{}` ou `{"type":"object"}` sans `required`/contrainte) ou **malformé**, cet oracle serait
**faux-vert** — acceptant tout, ne prouvant rien. Le byte-repro et les suites ne regardent jamais la
**force** du schéma → classe réellement ungatée. (NB l'oracle **tourne** sous `run_ci` en `python3`
nu, pas `-S` ; les 17 skips sont propres au harnais regression `python -S` — cf.
`regression-baseline-17-skips-by-design`.)

**Méthode.** Pour chacun des **26** `*.schema.json` : (1) `jsonschema.check_schema()` — est-il un
schéma Draft-N **valide** ? (2) heuristique de dents — déclare-t-il `required` OU (`properties` + ≥2
mots-clés de contrainte parmi required/enum/pattern/minItems/minimum/const/minLength/
additionalProperties) ? **Résultat : 26/26 valides, 0 WEAK.** Tous portent required+properties réels
(kw 3..103 ; le plus maigre `faisabilite/generator/brief.schema.json` req=1 props=10 kw=3 reste
non-vide).

**Dents de l'AUDIT prouvées** (une CLEAN d'un détecteur sans dents ne vaut rien) : le détecteur de
validité **mord** un schéma malformé (`required:"str"`, `properties.x:123`) ; le détecteur de dents
**mord** `{}` et `{"type":"object"}` ; aucun faux positif sur `rbac.schema.json` réel.

**Contrôles connexes (même session, tous CLEAN).**
- **Mapping `role_id → erpnext_role_name` = fonction** sur les 57 artefacts : **0 conflit** (un
  `role_id` ne résout jamais deux libellés distincts — sinon ERPNext créerait deux rôles). Complète
  `role-portail-referential-integrity-sweep` (qui résolvait les slugs, pas l'égalité des libellés).
- **Oracle jsonschema exécuté, pas skippé, sous `run_ci`** — vérifié sur `pie/manifest`
  (`test_schema_oracle ... ok`), `jsonschema 3.2.0` importable en `python3` nu.

**Verdict.** Couche oracle **genuinely constraining** ; **aucun défaut de production**. Balayage
lecture-seule, occurrence-zéro, rejouable → **pas un gate** (#5), sibling de
`runtime-encoding-integrity-sweep` / `role-portail-referential-integrity-sweep`.

**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate ajouté**
(#5), **0 chiffre saisi à la main** (#6 — `26`/`57` recomputés par balayage), **0 commande VPS** (#8).
`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Seules éditions : ce journal + mémoire
`schema-oracle-teeth-audit` (+ pointeur `MEMORY.md`).

---

## Session `20260806_104533` — FIX couverture · oracle orphelin `brief.schema.json` câblé à un test (le seul des 26 schémas non enforced) · +1 test · 4 artefacts régénérés · CI verte

**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` livrée/gatée, `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages
produit appartenant à Michel (non éditables worker). Plutôt qu'un énième sweep zéro-trouvaille,
recherche d'une **lacune de couverture réelle et corrigeable**.

**Axe.** Câblage des 26 oracles `*.schema.json` aux tests (distinct du *teeth-audit* qui prouvait
la non-vacuité des schémas, et de la *closedness* : ici « le schéma valide-t-il réellement
quelque chose ? »). Balayage `grep -rl <basename>.schema.json --include='*.py'` sur tout le dépôt :
**25/26 schémas référencés par au moins un `.py`** ; **1 orphelin** →
`faisabilite/generator/brief.schema.json` (**0 référence** dans aucun `.py`).

**Constat — orphelin authentique, pas by-design.**
- Le README du module documente `brief.schema.json` comme **« Contrat d'entrée »** (avec lien) et
  ses deux siblings (`version.schema.json`, `projets_master.schema.json`) SONT test-enforced
  (`test_version_json_validates_against_schema`, `test_full_pipeline_master_validates`).
- Le garde-fou runtime `faisabilite_gen.py::_validate_brief()` ne vérifie QUE le code `projet`
  (`P01..P09`), **jamais** la forme complète du brief (masterplan/architecture/typologies…).
- Donc un brief drifté hors contrat (type erroné, clé renommée/droppée) passait **inaperçu** :
  l'asymétrie vs les 2 siblings est le tell.

**Correction (teeth, pas churn).** Ajout de `test_input_briefs_validate_against_brief_schema`
dans `tests/test_generator.py` : valide `brief_complete.json` **et** `brief_incomplete.json` contre
`brief.schema.json` via le **validateur maison Publiciste** (zéro-pip → toujours exécuté, **pas** de
skip sous `python -S`, donc `skipped` reste 17). Vérifié empiriquement AVANT d'écrire le test : les
deux fixtures conforment sous le validateur maison **ET** l'oracle `jsonschema` (`errs=[]` / OK).
L'« incomplet » est conforme au **sens schéma** (null/champ absent admis, seul `projet` requis) ; il
n'est « incomplet » qu'au **sens sémantique** (prix→placeholders/rétrogradation · anti-invention #6).

**Cascade gatée régénérée** (générateurs, jamais à la main · #6) :
- `count_tests` (faits disque) → `regression_plan.json` : `faisabilite/generator` 16→**17**.
- `regression_gen.py build` + `run` → `regression_run.json` + `MANIFEST.json` : totaux
  **624→625** exécutés · **607→608** passés · **17** skippés inchangés · 24/24 vertes · PASS byte-repro.
- `audit_4big_gen.py build` → `quality_report.json` : evidence « 16 méthodes test_* » → **17** (24/24 ≥95).

**Prose gatée réalignée** (le gate `check-readme-claims` a mordu, comme prévu, sur 4 surfaces) :
README module (`16 tests`→`17`, + mention du contrat d'entrée), fiches
`03_agents/faisabilite/AGENT.md:22` (col Tests 16→17), `03_agents/qa/AGENT.md` (624/607→625/608),
`03_agents/erpnext_backend/AGENT.md` (624→625). **Prose ungatée present-tense réalignée** :
`GAP_ANALYSIS_SPRINT1.md` (16→17 tests). **Canal stakeholder** : nouveau bloc de session appendé à
`daily_reports/2026-08-06.md` (les 3 blocs de currency antérieurs = snapshots datés à leur HEAD,
**non réécrits** — leur 624/607 était juste à leur instant).

**Portée / anti-churn.** **0 gate ajouté** (#5 — le gate `check-readme-claims` existant a suffi à
prouver la cohérence, il a mordu sur les 4 fiches/README stales) · **0 chiffre saisi à la main**
(#6 — tous recomputés par les générateurs) · **0 commande VPS** (#8). `./run_ci.sh` =
**33 PASS · 0 FAIL · 0 SKIP**. Fichiers touchés : 1 test (+1 méthode) · 4 artefacts régénérés ·
5 surfaces prose · ce journal.

---

## Session `20260806_111534` — VÉRIF · axe NEUF « intégrité référentielle interne des `$ref` des 26 oracles-schémas » → CLEAN 108/108 refs · détecteur teeth-prouvé · 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 (Sprints 1→8 ; prod VPS / builds EAS /
dépôts ONAPI hors périmètre worker · #8). Registre `OPEN_DECISIONS_REGISTER.md` = arbitrages produit
**appartenant à Michel**, non éditables. Objectif anti-churn : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** Les 3 axes schéma déjà fermés portaient sur : la **non-vacuité**
(`schema-oracle-teeth-audit` — required + mots-clés de contrainte), la **fermeture/complétude**
(`schema-oracle-closedness` — `additionalProperties` + énumération des `properties`), et le **câblage**
(`schema-oracle-wiring` — chaque `*.schema.json` enforced par ≥1 test). Question **encore jamais posée** :
**tout `$ref` interne d'un oracle résout-il vers une définition existante ?** Un `$ref` pendouillant
(`#/$defs/Xxx` inexistant) est une **oracle-integrity failure** à double détente :
- côté oracle **`jsonschema`** (optionnel, skippé sous `python -S` / absent du runner Gitea pip-less) :
  quand le `$ref` est **atteint** pendant la validation, `jsonschema` **lève `RefResolutionError`** →
  le test passe de VERT à **ERROR/ROUGE** — mais seulement là où `jsonschema` est présent ;
- côté **validateur maison zéro-pip** (toujours exécuté) : un validateur qui **ignore** un `$ref`
  inconnu **passe silencieusement** → **tout le sous-arbre de contraintes derrière le `$ref` n'est
  jamais enforced** = **faux-vert**. (Un seul validateur maison honore réellement `$ref` :
  `publiciste/lib/validator.py` ; les autres modules ne valident le schéma complet que via l'oracle
  `jsonschema` optionnel — d'où l'enjeu.)

**Balayage (résolveur JSON-Pointer maison, stdlib pur).** Collecte récursive de tous les `$ref` des
26 `*.schema.json`, résolution de chaque pointeur `#/…` contre le corps du schéma (`$defs` /
`definitions`, dé-échappement `~0`/`~1`), plus contrôle du **type de la cible** (doit être un objet-schéma)
et de l'**inverse** (définitions orphelines jamais référencées = schéma mort, cosmétique) :

| Métrique | Résultat |
|---|---|
| Schémas portant `$ref` | **13 / 26** |
| Total `$ref` | **108** |
| `$ref` pendouillants (interne non résolu) | **0** |
| `$ref` externes involontaires (non-`#`) | **0** |
| Cibles non-objet-schéma | **0** |
| Définitions orphelines (`$defs`/`definitions` non référencées) | **0** |

→ **CLEAN 108/108** : chaque `$ref` résout vers un objet-schéma existant, et chaque définition déclarée
est effectivement référencée (aucune contrainte silencieusement contournée, aucun schéma mort).

**Détecteur teeth-prouvé (harnais /tmp, lecture seule — 0 fichier repo muté).**
1. **Mutation → morsure** : injection de `_MUTANT_DOESNOTEXIST` sur un `$ref` de
   `fixtures.schema.json` → le résolveur **signale bien 1 pendouillant** (`#/definitions/role_MUTANT…`).
   Preuve que le CLEAN 108/108 n'est pas un détecteur inerte.
2. **Preuve de la double détente** (pourquoi un pendouillant est un vrai faux-vert), au niveau mécanisme :
   - `jsonschema` **lève `RefResolutionError`** dès qu'un `$ref` pendouillant atteint est validé
     (schéma jouet `{"x":{"$ref":"#/$defs/Missing"}}` vs `{"x":1}`) → test ROUGE ;
   - un validateur maison naïf (type/required seuls, `$ref` ignoré) **renvoie `[]`** sur le même doc
     → sous-arbre **non enforced** = faux-vert.
   (Note : les oracles `jsonschema` valident des objets **reconstruits en mémoire**, pas les `out/*.json`
   à plat — cf. `verify-non-defects-reconciliation-traps` ; aucune paire schéma↔`out/`-plat ne valide à 0
   erreur, ce qui est **normal** et non un défaut.)

**Portée / anti-churn.** **CLEAN → 0 correction de production** (aucun oracle défectueux). **0 gate
ajouté** (#5 — occurrence-zéro, rejouable à la demande comme les autres balayages teeth :
`schema-oracle-teeth-audit`, `test-shadowing-sweep`, `runtime-encoding-integrity-sweep`). **0 chiffre
saisi en dur** · **0 commande VPS** (#8). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**.
Fichiers touchés : ce journal + 1 entrée mémoire (`schema-oracle-ref-integrity`). Le résolveur est
consigné ci-dessus, rejouable tel quel (stdlib pur, sans réseau).

---

## Session `20260806_114542` — VÉRIF · axe NEUF « le skip `python -S` de l'oracle jsonschema masque-t-il un écart enum/const/schéma vivant ? » + reverse-check membres d'enum fantômes → CLEAN (1 phantom {WAG} = NON-DÉFAUT) · 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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit appartenant à
Michel (non éditables). Objectif anti-churn : **5ᵉ axe teeth schéma jamais balayé**, complémentaire des
4 précédents (non-vacuité · fermeture/complétude · wiring · intégrité des `$ref`).

**Le soupçon.** Le gate exécute les suites sous **`python -S`** → l'import du tiers `jsonschema`
échoue → les **7 tests-oracle** `@skipUnless(_HAS_JSONSCHEMA)` (Draft-07 complet) comptent parmi les
**17 skips by-design** (cf. baseline régression 624 ran / 607 passed / 17 skipped). **Question** : ce
neutralisation masque-t-elle un écart **enum / const / type / required / $ref** réellement présent
dans les artefacts committés ? Et un `enum` de schéma **sur-énumère-t-il** vs les données (membre
fantôme = contrat partiellement aveugle sur la forme) ?

**Méthode & résultat — CLEAN.**
1. **Ré-exécution des 7 suites porteuses d'oracle avec `python3` nu** (vrai jsonschema 3.2.0, donc les
   `skipUnless` **s'exécutent réellement**) → **skipped=0, toutes OK** :
   dossier_vente 31 · workflow_vente 25 · commissions 25 · seo 36 · ecf_dgii 39 · confotur 44 ·
   apply_plan 16. L'oracle Draft-07 dé-skippé valide le bundle **correctement assemblé** de chaque
   module (assemblage propre au module, pas une reconstruction) → **0 divergence**.
2. **Validation jsonschema réelle** de `rbac/rbac_50_roles.json` vs `rbac/rbac.schema.json` →
   **0 erreur** (l'oracle « neutralisé » passe pour de vrai sur le contrat RBAC phare).
3. **Reverse-check membres d'enum fantômes** sur les enums de rôle : data ⊆ enum pour
   `famille` (9=9) · `portail` (6=6) · `scope_donnees` (4=4) ; `entite_principale` enum(9) ⊋ data(8),
   **seul fantôme = {WAG}**. `used-not-in-enum = ∅` partout (une valeur produite hors-enum casserait la
   validation → serait déjà attrapée). **{WAG} = NON-DÉFAUT** : WAG est une **entité canonique**
   CLAUDE.md §Entités (Helios RD sous WAG) ; l'`enum` est le **domaine autorisé**, pas l'ensemble
   observé — un enum-surset est correct (breadth de contrat), comme l'axe fermeture/`financement`.
4. **Teeth** — les détecteurs enum+const du validateur maison `publiciste/lib/validator.py:76-81`
   **MORDENT** : `portail='PHANTOM_PORTAIL'` hors-enum → flaggé (message exact) ; const
   `cible_rbac_roles` 50→999 → flaggé ; data propre → 0 erreur. Détecteur prouvé non-toothless.

Les 19 autres schémas voient leur `enum/const` **enforced par le validateur maison** (gère
enum/const/type/required/additionalProperties/pattern/$ref) contre le bundle in-memory == `out/`
committé (check_artifacts), wiring déjà prouvé par l'axe « wiring ». **Conclusion : le skip `-S` de
jsonschema ne masque AUCUN écart enum/const/schéma.**

**Périmètre.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** · **0 commande
VPS** (#8). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers touchés : ce journal +
1 entrée mémoire (`schema-oracle-enum-const-vs-S-skip`). Sweep rejouable tel quel (jsonschema 3.2.0
local ; stdlib pur sinon ; sans réseau) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_124544` — VÉRIF axe NEUF « atteignabilité transitive d'une assertion » (aucun test run-only faux-vert) → CLEAN 0/651 + 1 durcissement (implicite→explicite)

**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 (Sprints 1→7 ; prod VPS / builds EAS / dépôts
ONAPI hors périmètre worker · #8). Registre `OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit
appartenant à Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** `test-shadowing-sweep` couvre les tests **jamais exécutés** (dup de
nom/classe). Distinct et complémentaire : un test **exécuté** mais qui **n'atteint aucune assertion**
(run-only) reste vert même si le code casse = faux-vert. Détecteur AST (stdlib pur) : une méthode
`test*` est **édentée** si elle ne contient NI assertion directe (`self.assert*`/`self.fail`/
`assertRaises`/`pytest.raises`), NI appel levant (`jsonschema.validate`, `_validate(...)`,
`check_output/call`, `raise`), NI délégation à un helper de sa classe lui-même édenté-négatif
(`self._expect_error`/`_breaks`…), NI appel à une fonction nommée assert/expect/breaks/reject/check.
Résolution **transitive** du graphe d'appels intra-classe (sinon 40 faux positifs sur les batteries
d'injections négatives qui délèguent à `_expect_error`/`_breaks`).

**Dents prouvées.** Mutants : un `test_bad` qui ne fait que `print(compute(3))` → **MORD** (True) ;
`self.assertEqual(...)` → non signalé ; délégation helper (par set toothed OU par nom) → non signalé
(3/3 discriminés).

**Résultat & durcissement.** Sur **651 méthodes** de test : **1 seul** candidat →
`qa/acceptance/tests/test_acceptance.py::TestCLI.test_validate_exits_zero`, qui appelait `gen.main(
["validate"])` **sans assertion explicite**. Investigation : ce n'était PAS un faux-vert réel — il mord
**implicitement** via `SystemExit` (`cmd_validate`→`_validate_or_die`→`sys.exit(1)` si erreurs). MAIS
fragilité latente : un refactor remplaçant `sys.exit(1)` par un `return 1` **silencieux** rendrait le
test édenté sans que rien ne rougisse. **Durcissement** (strictement plus fort, aucun inconvénient) :
`self.assertEqual(gen.main(["validate"]), 0)` — mord désormais AUSSI sur le chemin retour-non-zéro, et
rend les dents **visibles** (aligné sur le style `test_build_writes_outputs` voisin). `main` retourne 0
en succès (`cmd_validate` → `return 0`), assertion correcte.

**Conclusion.** **CLEAN 0/651** après durcissement (le détecteur re-passé confirme 0 édenté). Axe
teeth-prouvé. Édition minimale : **1 test durci** (pas de logique de production touchée · #8), suite
`qa-acceptance` OK, `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + le test
durci + 1 entrée mémoire (`test-assertion-reachability-sweep`). Sweep **rejouable** (AST stdlib pur) —
**NON promu en gate** (#5, occurrence-quasi-zéro après fix).

## Session `20260806_154606` — VÉRIF · axe NEUF « intégrité temporelle des tokens datés » → CLEAN · 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 (prod VPS / EAS / ONAPI hors périmètre · #8),
`OPEN_DECISIONS_REGISTER.md` = 3 arbitrages produit de Michel (non éditables). Objectif : un **axe teeth
encore jamais balayé** (candidat ancres intra-doc `](#frag)` écarté = **occurrence-zéro**).

**Axe NEUF (absent de la mémoire).** Toutes les dimensions déjà couvertes portent sur des **valeurs**
(comptes, slugs, schémas, encodage NFC/byte). Jamais balayé : la **validité calendaire** des **tokens
datés** eux-mêmes. Un `2026-02-30` / `2026-13-01` / `20260840_…` se lit comme une date mais **n'existe
pas** = date **inventée / typo** — faux-vert direct contre **#6 « Zéro invention »**, invisible à tous
les gates (aucun ne parse les dates). Distinct de `two-logging-channels` (qui borne les *figures*, pas
la *forme des dates*).

**Détecteur (stdlib pur `datetime.date`, rejouable).**
- **ISO** `YYYY-MM-DD` : chaque token doit construire un `datetime.date` valide (mois 1-12, jour valide
  pour le mois, années bissextiles gérées par la stdlib).
- **Compact** `YYYYMMDD` (préfixe des session-ids `_HHMMSS`) : bornes **digit-only** `(?<!\d)…(?!\d)`
  pour capturer réellement `20260806_154606` (le `\b` naïf échoue car `_` est word-char = piège corrigé)
  sans faux-parser la portion heure `154606` (n'amorce pas « 20 »).
- **Cohérence journal** : chaque `05_activity_log/YYYY-MM-DD.md` — la date du **nom de fichier** doit
  égaler la 1re date ISO de son **en-tête**.

**Dents prouvées (mutants, prod intouchée).** Selftest synthétique : `2026-02-30`/`2026-13-01`/
`20260230_1200` **MORDENT**, `2026-08-06`/`20260806_1200` **épargnés**. Bout-en-bout sur fichier réel
(`05_activity_log/2026-08-06.md`) : baseline `[]` ; mutant `2026-08-46` → flagué ; mutant `2026-02-30`
→ flagué. 5/5 discriminés.

**Résultat.** Sur **379 fichiers** trackés (`.md/.json/.py/.yml/.sh/.txt`) : **289 tokens ISO** +
session-ids compacts → **0 date-calendaire invalide** · **0 mismatch** nom-fichier↔en-tête sur les
journaux `05_activity_log/`. **0 date inventée.** Faux-positifs écartés par construction : la portion
`HHMMSS` des timestamps (bornes digit-only), les entiers 8-chiffres non-datés hors fenêtre 2024-2027.

**Conformité.** 0 édition de production (sweep read-only). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL ·
0 SKIP**. Fichiers : ce journal + 1 entrée mémoire. Sweep **rejouable** (stdlib pur) — **NON promu en
gate** (#5, occurrence-zéro).

## Session `20260806_184624` — VÉRIF · axe NEUF « portabilité des NOMS de fichiers / structure de l'arbre git (collision-casse · réservés Windows · chars illégaux · dot/espace terminal · symlinks) » → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh --static` au démarrage = **8 PASS · 0 FAIL · 0 SKIP** (gate complet = 33 jobs), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** TOUS les balayages précédents portent sur le **contenu** des fichiers (comptes, slugs, schémas, encodage byte/NFC, dates) ou les **valeurs** des artefacts. Jamais éprouvé : la **portabilité de l'arbre lui-même** — les **noms de fichiers** et la **structure** du dépôt. Un checkout se fait sur macOS (case-insensitive/case-preserving) et Windows (NTFS : noms réservés, chars interdits, dot/espace terminal ignorés) — la CI, elle, tourne sous Linux (case-sensitive, POSIX permissif). Donc un futur agent qui commit `out/Data.json` à côté de `out/data.json`, ou un fichier `con.md`/`a:b.json`, garde la **CI verte sous Linux** tout en **cassant le `git checkout` sur Mac/Windows** (collision silencieuse : un des deux fichiers écrase l'autre / erreur d'invalid-argument). Faux-vert de portabilité, invisible à toute la chaîne de gates (aucun ne regarde les noms). Distinct de `runtime-encoding-integrity-sweep`/`unicode-nfc-normalization-sweep` (qui balaient le **contenu** octet/forme-NFC des fichiers, jamais leurs **chemins**).

**Détecteur (stdlib pur, rejouable).** Sur `git ls-files` (409 fichiers · 340 composants de chemin uniques) :
- **Collision-casse** : chemin complet ET (dirname + basename lower) — deux entrées `tolower`-égales mais littéralement distinctes.
- **Réservés Windows** : `CON/PRN/AUX/NUL/COM1-9/LPT1-9` (comparés sur le **stem** lower, avant la 1re extension — NTFS réserve aussi `nul.txt`).
- **Chars illégaux NTFS** : `< > : " \ | ? *`.
- **Chars de contrôle** : tout `ord(ch)<32` dans un composant.
- **Dot/espace TERMINAL** de composant (`c != c.rstrip(" .")`) — Windows les strippe silencieusement → renommage fantôme.
- **Longueur** : composant >255 octets, chemin >200 octets.
- **Symlinks trackés** (`git ls-files -s` mode `120000`) : cible relative ? dans le repo ? résout sur disque ?

**Dents prouvées (mutants synthétiques, prod intouchée).** 6 classes discriminées end-to-end : collision `out/Data.json`⟷`out/data.json` **MORD** ; `con.md`, `nul.txt` (réservés) **MORDENT** ; `a:b.json`, `we|rd.md` (illégaux) **MORDENT** ; `ta\tb.md` (ctrl) **MORD** ; `folder.`, `x.md.` (dot terminal) + `folder ` `name.md ` (espace terminal) **MORDENT** ; l'échantillon réel + le cas légal `name .md` (espace **interne**, autorisé) **épargnés**. Un premier mutant trailing-space mal formé (`name .md`) a été correctement **épargné** par le détecteur → mutant corrigé en composant à espace **terminal**, pas le détecteur (validation croisée).

**Résultat.** Sur **409 fichiers trackés** : **0 collision-casse** (chemin + basename), **0 nom réservé Windows**, **0 char illégal/contrôle**, **0 dot/espace terminal**, **0 composant/chemin surdimensionné**. **1 symlink** tracké — `04_roadmap/ROADMAP.md → ROADMAP_8_WEEKS_OR_LESS.md` : cible **relative**, **dans le repo**, **résout** vers un fichier existant = portable et sûr (ni absolu, ni hors-repo, ni dangling). Arbre git **portable**.

**Conformité.** 0 édition de production (sweep read-only). `./run_ci.sh --static` inchangé = **8 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + 1 entrée mémoire (`tree-filename-portability-sweep`). Sweep **rejouable** (`git ls-files` + stdlib pur) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_191634` — VÉRIF · axe NEUF « découverte & COLLECTE des tests par `unittest discover` (fichier `test*.py` · localisation sous `tests/` · classe héritant bien de `TestCase`) » → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP** (gate complet), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** La mémoire couvre déjà `test-shadowing` (méthodes/classes au **nom dupliqué**, silencieusement écrasées) et `test-assertion-reachability` (corps de méthode n'atteignant aucune assertion). Jamais éprouvé : le **mécanisme de COLLECTE** lui-même de `python3 -m unittest discover -s tests` — étape en amont de tout ce que la mémoire couvre. Trois faux-verts possibles, tous **invisibles à la chaîne de gates** (aucun ne modélise la découverte) :
- **(a) Nommage de fichier** : `discover` n'inclut que le motif par défaut `test*.py`. Un fichier `foo_test.py`/`tests_foo.py` contenant de vrais `TestCase` est **silencieusement non-découvert** → 0 test exécuté, gate vert.
- **(b) Localisation** : la CI passe `-s tests`. Un fichier `test_*.py` écrit **hors** d'un dossier `tests/` (à la racine du module, par erreur d'emplacement) n'est **jamais atteint**.
- **(c) Collecte de classe** : `discover` ne collecte que les sous-classes (transitives) de `unittest.TestCase`. Une classe portant des méthodes `test_*` mais dont la chaîne d'héritage **n'atteint pas** `TestCase` (oubli de la base, ou base intermédiaire elle-même non-TestCase) est **ignorée** → ses tests ne tournent jamais, comptés « présents » par grep mais **inertes**.

**Détecteurs (stdlib pur `ast` + `git ls-files`, rejouables).**
- (a) : sur les **25** dossiers `tests/`, chaque `*.py` non-`__init__` doit matcher `test*.py`.
- (b) : `git ls-files` — aucun `test*.py` tracké hors d'un segment `/tests/`.
- (c) : parse AST de chaque fichier de test ; pour chaque `ClassDef` portant ≥1 méthode `test*`, résolution **transitive intra-fichier** de la chaîne de bases jusqu'à un identifiant se terminant par `TestCase` (couvre `unittest.TestCase`, `TestCase`, `base.TestCase` importé).

**Dents prouvées (mutants synthétiques, prod intouchée).** (a) : les 25 fichiers matchent `test*.py`. (c) : selftest AST — `class TestX(unittest.TestCase)` **épargné** (0 problème) ; `class TestX:` sans base **MORD** (1) ; `class TestX(Base)` avec `Base` non-TestCase (chaîne cassée) **MORD** (1). 3/3 discriminés.

**Résultat.** **25 fichiers de test**, **113 classes** au total. (a) **25/25** fichiers découvrables (`test*.py`). (b) **0** fichier `test*.py` hors `tests/` (aucun test orphelin d'emplacement). (c) **0/113** classe test-porteuse sans `TestCase` dans sa chaîne — **toutes** les classes portant des `test_*` sont réellement collectées. **0 faux-vert de collecte.** Le périmètre exécuté par la CI = le périmètre réellement écrit.

**Conformité.** 0 édition de production (sweep read-only, harnais en `/tmp`). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers : ce journal + 1 entrée mémoire (`test-discovery-collection-sweep`). Sweep **rejouable** (`ast` + `git ls-files`) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_194635` — VÉRIF · axe NEUF « indépendance à l'ORDRE d'exécution des tests (couplage inter-test par état de classe/module partagé) » → CLEAN · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP** (gate complet), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel. Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** La mémoire couvre déjà, sur les suites : `test-shadowing` (méthodes/classes au nom dupliqué), `test-assertion-reachability` (corps n'atteignant aucune assertion), `test-discovery-collection` (le mécanisme de COLLECTE : nommage `test*.py`, localisation `tests/`, héritage `TestCase`). Jamais éprouvé : l'**indépendance à l'ORDRE d'exécution**. `unittest discover` exécute les méthodes en ordre **alphabétique croissant fixe** (`sortTestMethodsUsing` par défaut). Un test qui **lit** un état laissé par un test **alphabétiquement antérieur** (attribut de classe muté, variable de module, singleton) **passe** dans l'ordre fixe de la CI mais est **couplé** : c'est un faux-vert d'isolation, **invisible à toute la chaîne de gates** (aucun ne varie l'ordre) et distinct des 3 axes sœurs (nom / atteignabilité / collecte). Classe de bug réelle et classique (« order-dependent / flaky tests »).

**Détecteurs (stdlib pur `unittest`, rejouables, harnais `/tmp` — 0 fichier prod touché).** Pour chacun des **25** dossiers-module (cwd = dir module, `sys.path` répliquant `python3 -m unittest discover -s tests`), 3 exécutions comparées :
- **normal** : ordre CI (référence).
- **rev_method** : `sortTestMethodsUsing` **inversé** → renverse l'ordre des méthodes **intra-classe** (couplage inter-méthode d'une même classe).
- **rev_full** : inversion des méthodes **+** renversement récursif de l'arbre `TestSuite` (`_tests[::-1]`) → renverse aussi l'ordre des **classes** (couplage **inter-classe** : classe A alpha-première produit un état lu par classe Z). Purge des modules fraîchement importés entre chaque dir pour un ré-import propre.
Critère : `run/f/e` identiques aux 3 modes **et** 0 failure/error partout.

**Dents prouvées (mutants synthétiques, prod intouchée).** 3 discriminations end-to-end :
- Intra-classe : `test_a_sets` remplit `TestCoupled.state`, `test_z_reads` l'assert → **normal (4,0,0)** cache, **rev_method (4,1,0)** MORD. Suite propre voisine épargnée.
- Inter-classe : `SHARED` de module rempli par `TestA_Producer`, lu par `TestZ_Consumer` → **normal (2,0,0)** cache, **rev_method (2,0,0)** MISS (méthodes seules ne renversent pas l'ordre des classes), **rev_full (2,1,0)** MORD. Prouve que rev_full apporte une puissance de détection réelle au-delà de rev_method — les **deux** axes étaient nécessaires.

**Résultat.** Sur **25 suites · 651 tests** : **0 module order-dependent** sous rev_method **et** sous rev_full. `run/failures/errors` strictement identiques aux 3 ordres, 0 failure partout. **Aucun couplage inter-test caché** (ni intra-classe ni inter-classe) : le vert de la CI ne dépend pas de son ordre alphabétique fixe. Chaque test est réellement isolé.

**Conformité.** 0 édition de production (sweep read-only, harnais `/tmp`). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Fichiers : ce journal + 1 entrée mémoire (`test-order-independence-sweep`). Sweep **rejouable** (`unittest` stdlib pur) — **NON promu en gate** (#5, occurrence-zéro).

## Session `20260806_201644` — VÉRIF · axe NEUF « intégrité STATIQUE de la couche shell-script : parse-validité (`bash -n`) + fidélité du code de sortie des gates » → CLEAN 10/10 parse · 8/8 exit load-bearing · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP** (gate complet), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel (non éditables). Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** `runtime-encoding-integrity-sweep` a fait `compile()` sur les **144 `.py`** (parse-validité côté Python) mais **jamais `bash -n`** sur les scripts **shell** — la couche `ci/*.sh` + `run_ci.sh` n'a jamais été éprouvée en parse-validité. Deux sous-axes complémentaires, tous deux ciblant la **couche gate elle-même** (méta-niveau, aveugle aux autres sweeps qui vérifient le CONTENU) :
- **(A) parse-validité shell (`bash -n`)** — une erreur de syntaxe dans une **branche non-atteinte** d'un gate (ex. la branche `report()`/`bad()` du chemin ROUGE, jamais exercée tant que le repo est CLEAN) **passe silencieusement** : `bash -n` la détecterait, mais le runtime ne l'atteint jamais sur le chemin vert. Le jour où un vrai défaut fait entrer l'exécution dans cette branche, le gate **crashe (exit 2)** au lieu de reporter proprement — ou pire, l'erreur masque le check. Sœur-SHELL du `.py compile`.
- **(B) fidélité du code de sortie** — les 8 gates sont **accumulateurs** (`set -uo pipefail` **sans** `set -e` : `FAIL=0` ; `report()` pose `FAIL=1` ; `exit "$FAIL"` final). Un gate qui incrémenterait son compteur mais ferait `exit 0` (ou lirait la mauvaise variable, ou capturerait `rc=$?` **après un pipe** masquant l'exit d'un python) = **faux-vert de la CI elle-même** — le défaut de plus haut impact possible (rend TOUTE la couverture du gate inerte). Distinct de `guard-constraints-flags-usage` (qui mutation-teste INPUT→rouge, **empiriquement** ; ici je prouve **structurellement** que la ligne `exit` lit le bon accumulateur).

**Détecteurs (rejouables, harnais `/tmp`, 0 fichier prod touché).**
- (A) `bash -n` sur chacun des **10** scripts shell trackés (`git ls-files '*.sh'`).
- (B) audit de la ligne `exit` finale de chaque gate ; vérification que `check_readme_claims.sh` invoque son heredoc python en `python3 - <<'PY'` **non-pipé** (l.121) → `rc=$?` (l.9237) capture bien l'exit python, pas celui d'un `tee`.

**Résultat → CLEAN.** (A) **10/10 scripts parse OK** sous `bash -n` (`ci/*.sh` × 9 + `run_ci.sh`). (B) **8/8 gates** terminent par `exit "$FAIL"` (ou `exit "$rc"` pour readme-claims, `rc` = exit du python non-pipé) ; `lib.sh` n'a **aucun** `exit` top-level (fichier **sourcé**, correct — cohérent avec sa propre doc « jamais exécuté en propre »).

**Dents prouvées (bout-en-bout, `/tmp`, prod intouchée).**
- (A) copie de `validate_json.sh` + `if [[ 1 -eq 1 ]; then …` (un `]` manquant) → `bash -n` **PARSE-FAIL** (MORD) ; le vrai script **épargné** (OK). Détecteur non-inert.
- (B) mini-repo git `/tmp` avec un `sub/broken.json` malformé : le **vrai** `validate_json.sh` → **RED ✓** ; un **mutant** identique où `exit "$FAIL"` est remplacé par `exit 0` → **fake-green ✓**. Prouve que la ligne `exit` est **load-bearing** — le faux-vert que le sous-axe (B) chasse est un vrai risque, et le vrai gate y échappe.

**Conformité.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6 — 10/8 recomputés par balayage) · **0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN, occurrence-zéro, détecteurs rejouables ; transitivement, la fidélité de sortie est aussi empiriquement prouvée par le mutation-bite `guard-constraints-flags-usage`). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Fichiers : ce journal + 1 entrée mémoire (`shell-script-static-integrity-sweep`).

## Session `20260806_204654` — VÉRIF · axe NEUF « cohérence shebang ↔ bit-exécutable ↔ interpréteur sur l'arbre tracké » → CLEAN 409 fichiers · 0 édition de production

**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS · 0 FAIL · 0 SKIP** (gate complet), arbre propre, roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (prod VPS / builds EAS / dépôts ONAPI hors périmètre worker · #8), `OPEN_DECISIONS_REGISTER.md` = arbitrages produit de Michel. Objectif : un **axe teeth encore jamais balayé**.

**Axe NEUF (absent de la mémoire).** La session précédente a fait `bash -n` (parse-validité shell) + fidélité du code de sortie sur la couche gate ; `runtime-encoding-integrity-sweep` a fait BOM/CRLF/ctrl-char **au niveau octet** sur les sources. Jamais éprouvée : la **relation** ligne-shebang ↔ **bit-exécutable git** ↔ **interpréteur** — méta-axe distinct, aveugle à toute la chaîne de gates. Six faux-verts possibles :
- **(1) exec sans shebang** : un fichier `100755` sans `#!` → l'exécution directe échoue à trouver un interpréteur (fallback `sh` hasardeux).
- **(2) CRLF dans le shebang** : `#!/usr/bin/env bash\r` → « bad interpreter » à l'exécution (invisible au parse `bash -n` qui tolère le CR).
- **(3) BOM avant `#!`** : le shebang est **silencieusement désactivé** (le noyau ne le reconnaît plus en tête).
- **(4) mismatch interpréteur↔extension** : un `.sh` portant `python3`, ou un `.py` portant `bash`.
- **(5) shebang hors ligne 1** : une ligne blanche/commentaire avant `#!` le neutralise.
- **(6) dérive mode-git ↔ bit-disque** : `os.access(X_OK)` ≠ `100755` (fichier exécutable sur disque mais commité non-exec, ou l'inverse).

**Détecteur (stdlib pur, rejouable, `/tmp` — 0 fichier prod touché).** `git ls-files -s` (mode + chemin) + lecture des 4 premiers Ko de chaque fichier ; classification des 6 codes ci-dessus par fichier.

**Résultat → CLEAN sur 409 fichiers trackés.** **9 exécutables** (`100755` = les 8 `ci/*.sh` + `run_ci.sh`, tous `#!/usr/bin/env bash`), **46 shebangs** au total. Les **37 shebang-mais-non-exec** sont **par conception** — les `.py` (générateurs/tests) se lancent via `python3 fichier.py` / `-m` (jamais `./fichier.py`), et `ci/lib.sh` est **sourcé** (100644, garde un shebang doc-hint — cohérent avec son absence de `exit` top-level trouvée la session précédente). **0** exec-sans-shebang · **0** CRLF · **0** BOM · **0** mismatch-interpréteur · **0** dérive disque/git.

**Dents prouvées (mutants synthétiques `/tmp`, prod intouchée).** Harnais rejouant la logique de détection sur des fixtures `(mode, chemin, octets, disk_exec)` : **9/9 discriminations** — 3 fichiers propres (gate `.sh`, module `.py`, lib sourcée) **épargnés** ; 6 classes de mutants (exec-sans-shebang, CRLF, BOM, `.sh`→python, `.py`→bash, dérive-mode) **mordues**. Détecteurs non-inertes.

**Conformité.** VÉRIF pure : **0 fichier de production modifié** · **0 chiffre inventé** (#6 — 409/9/46/37 recomputés par balayage `git ls-files -s`) · **0 commande VPS** (#8) · **aucun gate ajouté** (#5 — axe CLEAN, occurrence-zéro, détecteur rejouable). `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Fichiers : ce journal + 1 entrée mémoire (`shebang-execbit-coherence-sweep`).
