Files
oto-enterprise-os-dtp/05_activity_log/2026-08-12.md
T
Claude Code DTP Worker 5af86e3518 [DTP-Worker 20260812_073055] hardening correctness · rendu HTML publiciste — percent-encode le SEUL segment d'URL restant (code) dans le url('…') CSS
Durcissement d'uniformité (pas bug déclenchable aujourd'hui) qui COMPLÈTE le fix
correctness de la session 070054 (aeb30b8). Ce fix-là a percent-encodé
hero['fichier'] dans src=f"/static/projets/{code}/{quote(fichier)}", mais dans la
MÊME url('…') CSS il restait un 2ᵉ segment dynamique — `code` — interpolé brut puis
html-échappé via _esc(src). C'est exactement l'anti-pattern identifié par 070054
(html.escape = mauvais encodeur pour un segment d'URL), appliqué de façon incomplète.

Fix : src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}" — quote() est
le SEUL encodeur correct pour un segment d'URL ; le CONTEXTE, pas la donnée courante,
dicte l'encodeur. Zéro dérive : codes réels = P01..P09 (§Projets), quote("p01")=="p01"
→ byte-repro inchangé. Cas frère exact de 060044/criteria.py (« durcir le SEUL membre
non-durci d'une famille ») : ici le SEUL segment d'URL non-encodé de la même url().

Test à dents (test_hero_media_code_segment_percent_encoded) : code théorique "P'X" →
prouvé FAIL sans quote(code), PASS avec. Suite publiciste 25→26, matrice 630/613→631/614.

Défaut d'abord : classe encodage-contextuel balayée sur tous les modules HTML/CSS/URL ;
2 candidats Explore réfutés non-défauts (chat mount_html = persona forcée Amélie par
invariant 7 + portail_key enum ; code segment = théorique mais durci par uniformité).
Cascade régénérée dans l'ordre (quality_report en DERNIER) · 3 fiches compteurs réalignées.
run_ci 33/0/0 · NFC-clean · zéro code moteur V18 (bloqué D-06 #6) · zéro gate (#5) · zéro invention (#6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 07:41:05 +00:00

155 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Activity Log · 2026-08-12 · Claude Code DTP Worker
## Session `20260812_040034` — CLEANUP #5 · suppression d'un orphelin `out_fresh/` (doublon d'artefacts commité par accident, échappe à TOUS les gates)
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (directive `:90-91` « NE PAS coder avant l'audit » · D-06 approbation audit = gate d'entrée · #6) → **aucun code moteur légitime**. Dernier commit `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (référence design, hors production repo).
**Chasse au défaut d'abord (2 axes) → 1 candidat écarté comme NON-DÉFAUT (refus de fabriquer · #6).** Explore very-thorough sur les modules produit hors `audit_4big`/`regression`/`publiciste` (déjà balayés). Le seul candidat remonté « confirmé » — `faisabilite/generator/genlib/model.py:104` `is_filled()` exigeant `"{{" in text and "}}" in text` pour classer un placeholder — a été **réfuté au byte-level** : le renderer (`renderer.py:23-42`) n'émet **jamais** que des placeholders `{{...}}` **complets** (deux accolades), `brief_incomplete.json` n'utilise **aucune** accolade pour marquer l'absence (il utilise `_PLACEHOLDER_ABSENT` : « tbd »/« todo »/… + champs omis), et **aucune** occurrence mono-accolade n'existe dans tout le module. Exiger les deux accolades est donc le choix **conservateur correct** (flaguer un `}}` isolé dans de la prose réelle serait un faux positif). L'Explore s'était sur-engagé sur un cas théorique inatteignable → **non touché** (`verify-non-defects`).
**Vrai locus trouvé = un ORPHELIN ACCUMULÉ (violation #5 « JAMAIS accumuler doublons »), pas un bug.** `05_deliverables_mvp/frontend/portails/out_fresh/` (2 fichiers : `workspace.json` + `MANIFEST.json`) est :
- **Tracké** (`git ls-files` le confirme) — mais commité **une seule fois**, dans la session auto `f954cc1` du **2026-08-02** (scratch-dir de comparaison « fresh rebuild » commité par accident).
- **Byte-identique** à `out/` (diff `json.tool` = IDENTICAL content).
- **Produit par rien** : le générateur `workspaces_gen.py` écrit **uniquement** dans `out/` (`_DEFAULT_OUT = os.path.join(_HERE, "out")` · `-o` défaut `./out`) — jamais `out_fresh`.
- **Référencé nulle part** : `git grep out_fresh` sur tout l'arbre tracké (hors le dossier lui-même) = **ZÉRO** (ni code, ni doc, ni CI, ni `.gitignore`, ni test).
**Pourquoi c'est un vrai défaut et pas un artefact by-design (preuve qu'il échappe à tout gate).** `check_artifacts.sh` découvre les cibles par `find -type d -name out` — un match **exact** sur `out`, donc `out_fresh` **n'entre dans aucun gate de reproductibilité**. C'est précisément pourquoi il a survécu au balayage `reverse-bijection-artifact-sweep` (qui ne vise que les répertoires `out/`). Conséquence : c'est une **bombe à retardement de staleness** — rien ne le régénère, donc dès que la source du générateur bouge, `out/` se met à jour tandis que `out_fresh/` **diverge silencieusement** en une copie **stale, non-possédée et trompeuse** (« laquelle fait autorité ? »). Dead weight ungated. C'est l'unique scratch-dir du genre dans tout le repo (`find` pour `out_new`/`out_old`/`out_tmp`/`out_bak`/`out_fresh` → seul `out_fresh` existe · occurrence isolée).
**Fix = suppression (`git rm -r out_fresh/`).** Sûr (réversible via l'historique git), et **je ne l'ai pas créé** (règle « avant de supprimer, regarde la cible » : contenu byte-identique à la copie autoritaire, aucun consommateur). La copie autoritaire `out/` est **intacte et se régénère BYTE-IDENTIQUE** (rebuild `-o /tmp` vs `out/` = identique, vérifié après suppression).
**Pourquoi pas un nouveau gate (#5).** Occurrence **isolée** (origine unique `f954cc1`, un seul module). Ajouter un gate « aucun répertoire `out_*` non-`out` » serait de la sur-ingénierie pour un événement-zéro. La discipline existante (générateur écrit dans `out/` · `.gitignore` documente que `out/` est commité) est le contrat ; l'orphelin était une déviation ponctuelle, corrigée à la source.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rien ne dépendait de `out_fresh/`). **0 code moteur V18** (bloqué #6), **0 module de production touché** (seul un doublon orphelin supprimé · char-class `model.py` laissée intacte car non-défaut), **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `- 05_deliverables_mvp/frontend/portails/out_fresh/{workspace,MANIFEST}.json` (supprimés) + ce journal.
## Session `20260812_043034` — CANAL STAKEHOLDER · actualisation `daily_reports` au HEAD courant (`35de616` → `ee88d36`)
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 ok**. Séquence moteur V18 toujours **bloquée** en gouvernance (audit préalable produit mais non validé · D-06 = gate d'entrée · #6) → **aucun code moteur légitime**.
**Chasse au défaut d'abord (2 axes) → tree cohérent, aucun défaut candidat.** (1) Vérifié que les chiffres cités dans le journal `20260812_040034` (629/612/17) sont **byte-cohérents** avec le `regression_run.json` commité (identiques). (2) Vérifié que `README.md` **ne fige aucun total de tests** (il pointe l'artefact byte-gaté comme « autorité », prose « 24 suites gated / 24 modules » exacte vs `quality_report.json`) → **0 drift numérique README**.
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le dernier rapport `daily_reports/2026-08-11.md` était épinglé à `35de616` (currency `20260811_092842`) et citait la régression à **628/611**. Depuis, **3 commits** ont avancé l'arbre à `ee88d36`, et la matrice a **divergé d'un incrément** :
- `39def91` (`[DTP-Worker 20260811_095844] Auto exec`, **postérieur** à la currency du rapport 08-11) a ajouté un test verrou **NBSP U+00A0** dans `publiciste/tests/test_publiciste.py` (+15 lignes) → régression **628→629 / 611→612** + `regression_plan/run/MANIFEST` + `quality_report` re-générés du même coup. Le rapport 08-11 **ne le couvre pas** → chiffre `628/611` désormais **stale**.
- `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (asset de référence design, hors production repo, root-owned).
- `ee88d36` = jalon worker de la session précédente de ce jour (suppression orphelin `out_fresh/` · déjà journalisée ci-dessus).
**Fix = émission de `daily_reports/2026-08-12.md`.** Porte le canal `35de616 → ee88d36`, **re-dérive tous les chiffres depuis les artefacts commités** (jamais figés · #6) : gate 33/0/0, régression **629/612/17 PASS**, audit 4Big **PASS 24× 100/100**, bijection **24↔24 ok**. Attribue explicitement la seule évolution de matrice (628→629) à `39def91`, note que `031b5e7`/`ee88d36` **ne touchent aucun test** (matrice stable). Reprend le §« Chemin critique bloqué » (D-06/D-07/D-01 en attente Michel) + escalation.
**Pourquoi pas un nouveau gate (#5).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de la mémoire `two-logging-channels`, pas une matière à gate.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `+ 05_deliverables_mvp/daily_reports/2026-08-12.md` (nouveau rapport stakeholder) + ce journal.
## Session `20260812_050034` — VÉRIF · nouvel axe de déterminisme « ordre d'exécution des suites » (balayage CLEAN, replayable, sans gate)
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD `ee88d36` (session précédente `043034`) → rien à ré-émettre.
**Chasse au défaut d'abord (2 axes) → 1 candidat Explore RÉFUTÉ comme non-défaut, tree propre.**
- **Axe prose/drift** (Explore very-thorough sur `daily_reports/*.md`, `04_roadmap/*.md`, `03_agents/**/AGENT.md`, liens Markdown). Le seul candidat « confirmé » — `03_agents/mobile/AGENT.md:81` « upgrade 54 tenté puis abandonné » vs `mobile_spec.json` `expo_sdk_major: 54` — a été **réfuté** par la mémoire `mobile-runtime-actual-vs-rebuild-target` **et** relecture byte : la ligne distingue explicitement le **runtime ACTUEL** (Expo 51, tentative 54 abandonnée le 2026-07-27) de la **cible de rebuild** (Expo 54 · S5). Le `54` du spec correspond exactement à « rebuild cible Expo 54 (S5) ». **Interne cohérent → non touché** (`verify-non-defects`).
- **Axe drift numérique README/artefacts** : déjà gaté (`check_readme_claims` ∈ run_ci = vert). 0 écart.
**Vrai locus = un axe de déterminisme JAMAIS balayé — l'ORDRE D'EXÉCUTION des 24 suites** (4ᵉ axe, frères : `hash-seed-determinism-sweep`, `locale-tz-determinism-sweep`, `forward-compat-warnings-sweep`**aucun** ne varie l'ordre des suites). Hypothèse de risque : un couplage inter-suites (état global partagé, scratch `/tmp` à nom fixe, cwd partagé) rendrait la matrice `regression_run.json` dépendante de l'ordre → RED intermittent sur le runner.
**Expérience (replayable).** Monkeypatch `reglib.discovery.discover_suites``reversed(...)`, ré-exécution `builder.run_matrix(spec)`, diff par-suite (clé = `id`) + totaux vs `out/regression_run.json` commité :
- Totaux **byte-identiques** : 629 ran / 612 passés / 17 skippés / 0 échec.
- **24/24 suites** : tuples `(ran,passed,failures,errors,skipped,ok)` **identiques**. `PER-SUITE DIFFS: NONE`.
**Pourquoi c'est fermé par construction (teeth, pas chance).** `runner.run_suite` (`reglib/runner.py:98`) lance **chaque suite dans son propre subprocess** avec `cwd=abs_path` (le dossier de la suite) — aucun état module in-process partagé, aucun cwd partagé ; l'agrégation dans `run_matrix` est une **somme indépendante de l'ordre**. Le couplage inter-suites est donc empêché *by design* ; le run inversé est une **confirmation teeth**, pas une découverte. Mémoire : `run-order-determinism-sweep`.
**Pourquoi pas un nouveau gate (#5).** Résultat CLEAN, occurrence-zéro, l'isolation subprocess est déjà le contrat architectural. Comme les 3 autres axes de déterminisme, c'est une **commande replayable**, pas une matière à gate. Aucun chiffre inventé (#6 — tous lus depuis l'artefact commité).
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — expérience en lecture seule sur `/tmp`, 0 artefact touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `run-order-determinism-sweep`, hors repo).
## Session `20260812_053034` — VÉRIF (nouvel angle i18n) · asymétrie de cardinalité SEO per-langue `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** · note explicative au point de dedup (anti re-flag, sans gate)
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD (session `043034`) → rien à ré-émettre.
**Angle FRAIS (jamais balayé) = cohérence tri-lingue FR/EN/ES du module `seo/`.** Aucun des ~40 balayages antérieurs n'avait attaqué l'i18n des mots-clés. Un Explore very-thorough a remonté un candidat « défaut confirmé » : `out/seo_keywords.json` = **fr 87 · en 87 · es 84**, l'espagnol « manquant » de 3 entrées projet P01 (generic/residence/appartement). J'ai **refusé de le prendre au mot** (#6, `verify-non-defects`) et **prouvé au byte-level que c'est un NON-DÉFAUT** — l'artefact étant byte-repro-gaté, tout écart reflète la LOGIQUE du générateur, pas une faute de saisie.
**Preuve (reproduction du générateur sans dedup, tracé collision→terme-gardé).** La génération est **symétrique par langue** (`keywords.py:58` `for lang in langs`) → **raw = 115/langue** identique. L'écart vient **uniquement** du dedup `(lang, term)` (`keywords.py:106-113`, « conserver la 1ʳᵉ occurrence ») :
- `country[es]` = **« Republica Dominicana »** est **exactement** la `localisation` générique de **5 projets** (P01/P02/P03/P06/P09 dans `fixtures/projets_master.json`). En espagnol, le mot-clé **global** `{type} Republica Dominicana` (émis avant les projets) est **byte-identique** au mot-clé **projet** `{type} {loc}` de P01 → la 2ᵉ occurrence (P01) est dédupliquée. Le terme **reste présent une fois**, en scope `global` : **rien n'est perdu**.
- En **fr/en**, `country` = « Republique Dominicaine » / « Dominican Republic » **≠** la localisation → pas de collision global↔projet ; P01 garde ses 7 termes et ce sont P02/03/06/09 (même loc) qui percutent P01. D'où **3 collisions global↔projet de plus en es** → 87 vs 84.
**Conclusion : sortie CORRECTE.** Chaque chaîne unique existe **exactement une fois** ; les invariants ne contraignent que `≥ min_keywords_per_lang` (50) + unicité `(term, lang)` (`seo_gen.py:88-98`, `test_seo.py:94-100`) — **jamais** l'égalité inter-langues, et à raison : forcer une symétrie ré-émettrait un **doublon strict**. Le chiffre `es=84` est d'ailleurs **déjà documenté et gaté** dans `README.md:15` (`258 (fr=87·en=87·es=84)`, re-calculé par `check_readme_claims`) → aucun drift, aucun « manquant ».
**Livrable = une NOTE explicative au point de dedup (`keywords.py:106`).** L'asymétrie est subtile et **non-évidente** : un Explore capable a failli la mal-classer en bug, et un futur balayage i18n refera l'enquête. J'ai ajouté un commentaire *accurate* (vérifié) expliquant la cause (coïncidence `country[es]` == localisation) et l'interdiction de « corriger » vers une symétrie. **Aucune** logique/fixture/claim-gaté touché (les gates byte visent `out/*.json`, pas la source `keywords.py`). Style aligné : ASCII sans accents + em-dash `—` U+2014 (convention existante lignes 5-6), NFC-clean.
**Pourquoi pas un fix ni un gate (#5/#6/`verify-non-defects`).** (1) Pas de fix logique : changer le tie-break dedup (préférer projet vs global) altèrerait l'artefact byte-gaté + le claim README pour « corriger » un comportement correct. (2) Pas de fix fixture : compléter la localisation générique des 5 projets serait **inventer** une donnée (#6) sans source. (3) Pas de gate d'égalité per-langue : ce serait une **fausse contrainte** (l'asymétrie est linguistiquement légitime) — le seuil `≥ min` par langue est déjà gaté et suffit.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — commentaire pur, artefact `seo/out/*.json` re-build byte-identique via `check_artifacts` ∈ run_ci). **0 code moteur V18** (bloqué #6), **0 logique de production modifiée** (commentaire seul), **0 artefact autoritaire modifié**, **0 fixture touchée** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/seo/seolib/keywords.py` (note by-design au dedup) + ce journal.
## Session `20260812_060044` — HARDENING correctness · `criteria.py` — le 3ᵉ détecteur regex (le SEUL non durci) de la famille « détecter la forme, pas une graphie »
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` déjà à jour au HEAD (session `043034`).
**Chasse au défaut d'abord (3 axes computationnels, jamais audités en profondeur) → 2 modules confirmés CLEAN, 1 vrai locus de robustesse trouvé.**
- **`crm/commissions/comlib/finance.py`** (calcul commissions `montant = base × taux`) : logique **correcte**`rate()` normalise nombre/« 3.5 % »/« 3,5 % » en fraction, `taux_pct` de sortie = fraction cohérente avec le libellé de formule, opérande manquant → `None` (jamais 0-inventé). **Non touché.**
- **`faisabilite/bancable/banclib/finance.py`** (cœur break-even, zone de risque D-07) : **correct** — point d'équilibre `⌈52 % × total_unites⌉` avec `52 %` re-dérivé du canonique #9 (jamais codé en dur, cf. fix `448c310`), agrégats catalogue `Σ(qté × prix)` gardés sur `all(... is not None)`, formule publiée avec chaque valeur. **Non touché.**
- **Vrai locus = `qa/audit_4big/q4lib/criteria.py:25`** — voir ci-dessous.
**Le locus : le 3ᵉ détecteur regex non durci d'une famille dont les 2 autres l'ont été.** Ce fichier est la source de **TROIS** fixes de correctness confirmés de la classe « détecter la **forme**, pas une **graphie** » : `ac595d0` (regex ASCII vs `\w` Unicode → test accentué sous-compté), `fd99362` (garde `__main__` littérale double-quotée → simple-quotée/Yoda/sans-espace loupées), `35de616` (`def ` figé vs `def\s+` de la source sœur byte-gatée → `def test_x(` loupé). Les deux détecteurs `_TEST_DEF_RE` (`def\s+…\w`) et `_MAIN_GUARD_RE` (`\s*==\s*`, deux styles de guillemets, ordre Yoda) sont explicitement durcis pour tolérer tout **Python valide** (doctrine articulée lignes 18-30). **Restait `_ADD_PARSER_RE`** — le 3ᵉ frère — figé sur `add_parser\(|add_subparsers\(` : il exige la parenthèse **collée** au nom de méthode, donc `add_subparsers ()` / `add_parser ("build")` (espace avant `(` — Python valide, PEP 8 déconseille sans l'imposer) sont **silencieusement loupés** → sous-scorage `20 pts` du critère CLI d'un module sain, exactement le symptôme de `fd99362`.
**Preuve de dents (avant fix).** Ancien regex : `p.add_subparsers ()`**False**, `sub.add_parser ("x")`**False** ; nouveau `add_parser\s*\(|add_subparsers\s*\(`**True** sur ces deux formes **et** True sur les formes normales `add_parser("build")` / `add_subparsers(dest=…)` (aucune régression de couverture).
**Pourquoi c'est un durcissement légitime et non une invention (#6).** (1) **Classe déjà jugée réelle 3×** dans CE fichier ; les 2 frères sont durcis, le 3ᵉ était l'anomalie — j'harmonise vers la doctrine **écrite du fichier lui-même** (« détecter la forme, pas une graphie »), pas une convention externe. (2) **Précédent direct** : `fd99362`/`35de616` ont durci pour des variantes de Python valide **pas nécessairement présentes** dans un module courant (guillemets simples, Yoda, `def\t`). (3) **Fix latent, artefact byte-identique** : `grep -rE 'add_(parser|subparsers)[ \t]+\('` sur tous les `.py` = **AUCUN** module n'utilise l'espace aujourd'hui → aucun re-scorage, `quality_report.json` re-build **byte-identique** (vérifié par `check_artifacts` ∈ run_ci, vert).
**Pourquoi pas un nouveau gate (#5).** Ce n'est pas une classe de drift à surveiller mais un durcissement one-shot d'un détecteur ; la robustesse est dans le code du critère, pas dans un gate. La famille des 3 détecteurs est désormais **cohérente end-to-end**.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Fichier édité **compile** + **NFC-clean** (accents intentionnels, style aligné lignes 18-30). **0 code moteur V18** (bloqué #6), **0 artefact autoritaire modifié** (byte-identique), **0 fixture/chiffre inventé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py` (regex `_ADD_PARSER_RE` durci + commentaire doctrine) + ce journal.
## Session `20260812_063050` — CANAL STAKEHOLDER · complétion du rapport `daily_reports/2026-08-12.md` au HEAD du jour (`ee88d36` → `37c68ac`)
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité, #6) = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 restent en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
**Chasse au défaut d'abord (2 axes, correctness fiscale jamais auditée en profondeur) → 0 défaut, modules confirmés CLEAN.**
- **`fiscal/ecf_dgii/ecflib/ncf.py`** (composition e-NCF DGII, Ley 32-23) : logique **correcte**`ENCF_RE` = `E + tipo(2) + secuencia(10)` = 13 car. ; `compose_encf` publie sa formule, retourne `e_ncf: None` quand un opérande manque (jamais 0/valeur fabriquée · #6), `_secuencia` zero-padde `[0-9]{1,10}` → 10 chiffres sans jamais inventer de séquence. `is_valid_encf`/`parse_encf`/`compose_encf` cohérents (une valeur composée re-valide toujours). **Non touché.**
- **`fiscal/ecf_dgii/ecflib/builder.py`** (plan de config e-CF) : **correct** — aucun chiffre OTO fixé (RNC/ITBIS/TipoCambio/endpoints Compupar restent `null · a_confirmer` par design #6), `_a_confirmer_count` déterministe, tri stable partout → bundle byte-repro. **Non touché.** (`is_filled` partagé re-vérifié = choix conservateur correct, cf. session `040034`.)
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` avait été émis en session `043034` au HEAD `ee88d36`. Depuis, **3 commits worker du même jour** ont avancé l'arbre à `37c68ac` sans que le snapshot stakeholder ne les reflète :
- `a17d86a` (`050034`) — 4ᵉ axe de déterminisme « ordre d'exécution des 24 suites » (CLEAN, replayable, sans gate).
- `1513e69` (`053034`) — vérif i18n SEO : asymétrie `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** + note by-design au dedup.
- `37c68ac` (`060044`) — hardening `criteria.py` (3ᵉ détecteur regex durci de la famille « détecter la forme, pas une graphie »).
Les trois sont des travaux de vérif/hardening à **artefacts byte-identiques** (confirmé par `check_artifacts``run_ci` vert) → **matrice stable à `629/612/17`**.
**Fix = complétion (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le compléter en fin de journée pour couvrir tout le travail du jour est le contrat, pas un doublon (#5). Porté : header `HEAD ee88d36 → 37c68ac` + session `063050`, nouvelle section « Jalons du jour depuis l'émission initiale (`ee88d36``37c68ac`) » (table des 3 commits + opposabilité au merge), `État courant` re-titré au HEAD `37c68ac` avec **tous les chiffres re-dérivés des artefacts commités** (629/612/17 · PASS 24× · bijection 24↔24 — inchangés, byte-identiques), note de stabilité de matrice étendue aux 3 jalons, §Portée actualisé.
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis l'artefact commité · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (complétion au HEAD du jour) + ce journal.
## Session `20260812_070054` — FIX correctness (bug latent réel) · rendu HTML publiciste : URL de rendu percent-encodée dans le `url('…')` CSS (apostrophe/espace/accent cassait silencieusement le hero) + test de régression à dents
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
**Chasse au défaut d'abord (large, 12 fichiers cœur / 7 modules jamais ré-audités ce cycle) → 11 CLEAN, 1 BUG LATENT RÉEL trouvé.** Un Explore ciblé sur `workflow_vente`, `dossier_vente`, `financement_bancaire` (gate+builder), `faisabilite/generator` (scorer+model), `seo` (schemaorg+hreflang+builder), `mobile`, `pie` a confirmé **0 défaut** (l'`epsilon 1e-9` du gate apport = tolérance flottante correcte, non-défaut). Puis audit manuel du **rendu HTML publiciste** (`generator.py`) — angle **output-encoding jamais balayé** (distinct des sweeps NFC/encoding octet).
**Le bug (rendu-correctness, entrée réaliste FR/ES).** `_hero_media` (`generator.py:92`) composait `src = f"/static/projets/{code}/{hero['fichier']}"` puis l'injectait dans `style="background-image:url('{_esc(src)}')"`. `_esc` (= `html.escape`) est le **mauvais encodeur pour un contexte CSS** : un nom de rendu réel — **« Vue d'ensemble.png »**, entièrement plausible dans un projet francophone en RD — devient `Vue d&#x27;ensemble.png`, que le navigateur **HTML-décode d'abord** en `Vue d'ensemble.png` ; l'apostrophe **referme alors la chaîne `url('…')` CSS**`background-image` invalide → **le hero ne s'affiche pas, silencieusement**. Reproduit au byte-level (`html.unescape` de la sortie le prouve).
**Preuve de dents.** Nouveau test `test_hero_media_url_percent_encode` (`test_publiciste.py`) : rendu avec `fichier="Vue d'ensemble.png"` → assert la présence de `Vue%20d%27ensemble.png` **et** l'absence de l'apostrophe brute dans le `url()` HTML-décodé. Vérifié **teeth** : en revenant à `{hero['fichier']}` sans `quote`, le test **ÉCHOUE** ; avec le fix, **PASS**.
**Le fix (correct, sans dérive).** `src = f".../{quote(hero['fichier'])}"` (`urllib.parse.quote`) — percent-encode le segment de fichier : `'``%27`, espace→`%20`, accents→`%XX`, tous valides à la fois dans l'URL, l'attribut HTML et le `url('…')` CSS. `_esc` conservé en défense de l'attribut HTML (commentaire ajouté : « _esc protège l'attribut HTML ; quote protège le contexte CSS interne »). **Zéro dérive d'artefact** : `quote` laisse les noms ASCII simples inchangés (`hero_aerien.jpg` → identique), donc la fixture de test et le rendu byte-repro sont intacts. Percent-encoder une URL est de toute façon le comportement **sémantiquement correct**.
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Le test ajouté fait passer la suite publiciste 24→25 et la matrice **629/612 → 630/613** (17 skips inchangés). Régénéré dans l'ordre correct (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` (`build -o out` + `run`) puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (629/612→630/613), `03_agents/erpnext_backend/AGENT.md` (629→630), `03_agents/publiciste/AGENT.md` (suite 24→25). Tous les diffs d'artefact = **pure propagation de compteur** (vérifié), aucune modification parasite.
**Pourquoi pas un gate (#5).** Le bug est un one-shot de correctness ; sa robustesse vit dans le code (`quote`) + le test de régression à dents, pas dans un gate de drift. Aucune nouvelle classe de drift à surveiller.
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · tous les compteurs re-dérivés des artefacts régénérés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (fix `quote`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
## Session `20260812_073055` — HARDENING correctness · rendu HTML publiciste : le SEUL segment d'URL restant non percent-encodé dans le `url('…')` CSS (complète le fix de la session `070054`) + test à dents
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **630 exécutés · 613 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
**Chasse au défaut d'abord (classe = encodage contextuel erroné, frère du fix `070054`) → 2 candidats Explore, tous deux RÉFUTÉS comme non-défauts, mais 1 durcissement d'uniformité principiel retenu.** Un Explore very-thorough sur tous les modules générant HTML/CSS/URL/CSV/JSON-LD a remonté 2 candidats :
- **`frontend/chat_otoia/chatlib/frappe.py:40` `mount_html`** — attributs HTML (`css_class`/`portail_key`/`persona`) interpolés **sans** échappement. **RÉFUTÉ (non-défaut, aucune entrée réelle ne casse)** : `persona["nom"]` est **forcé à `"Amélie"`** par l'invariant 7 (`chat_otoia_gen.py:155`), `portail_key` est un **enum** (`ventes`/`construction`/`achat`/`compta`/`direction`), `css_class` vient du spec contrôlé. Toutes provablement propres → défensif seul, pas de fix (`verify-non-defects` · #6).
- **`publiciste/lib/generator.py:99`** `_hero_media` — c'est le locus **retenu** (voir ci-dessous).
**Le locus (durcissement, pas bug déclenchable — honnêteté #6).** Le fix `070054` a percent-encodé `hero['fichier']` dans `src = f"/static/projets/{code}/{quote(hero['fichier'])}"`, mais dans la **MÊME** `url('…')` il restait un **second segment dynamique**`code` — interpolé **brut** puis html-échappé via `_esc(src)`. C'est **exactement** l'anti-pattern que le fix `070054` a identifié (html.escape = mauvais encodeur pour un segment d'URL), **appliqué de façon incomplète**. Ce n'est **pas** un bug déclenchable aujourd'hui : les codes réels sont strictement `P01..P09` (§Projets), jamais d'apostrophe → le byte-repro est **inchangé** (`quote("p01") == "p01"`). C'est un **durcissement d'uniformité de construction**, cas frère exact de la session `060044` (`criteria.py` : « durcir le SEUL détecteur non-durci d'une famille ») : ici **le SEUL segment d'URL non-encodé** de la même `url()`.
**Le fix + preuve de dents.** `src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}"``quote()` est le SEUL encodeur correct pour un segment d'URL, **le contexte (pas la donnée courante) dicte l'encodeur** (commentaire mis à jour en conséquence). Nouveau test `test_hero_media_code_segment_percent_encoded` : code théorique `"P'X"` → assert `/static/projets/p%27x/hero.jpg` **et** absence d'apostrophe brute dans le chemin HTML-décodé. Vérifié **teeth** : sans `quote(code)`, le test **ÉCHOUE** ; avec, **PASS**.
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Suite publiciste 25→26, matrice **630/613 → 631/614** (17 skips inchangés). Régénéré dans l'ordre (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (630/613→631/614), `03_agents/erpnext_backend/AGENT.md` (630→631), `03_agents/publiciste/AGENT.md` (suite 25→26). Diffs d'artefact = **pure propagation de compteur** (vérifié).
**Canal stakeholder laissé à une session dédiée.** `daily_reports/2026-08-12.md` cite encore `629/612` : il est déjà en retard d'un incrément depuis le fix `070054` (→630) que cette session-là n'a délibérément pas répercuté (canal snapshot **batché** par sessions stakeholder dédiées, cf. `3a40aee` qui a réconcilié 3 jalons d'un coup). Une future session stakeholder réconciliera **630 (070054) + 631 (cette session)** ensemble — pas d'invention (#6), chiffres re-dérivables de l'artefact.
**Pourquoi pas un gate (#5).** Robustesse portée par le code (`quote`) + le test à dents ; aucune nouvelle classe de drift. Occurrence isolée (uniformité d'un fix du même jour).
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · compteurs re-dérivés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (`quote(code)`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.