Files
oto-enterprise-os-dtp/05_activity_log/2026-08-06.md
T

1545 lines
131 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Activity Log · 2026-08-06 · Claude Code DTP Worker
## 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` =
`requiredpaid` = 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
`D800DFFF`) → **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
(154=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).