# Activity Log · 2026-08-05 · Claude Code DTP Worker ## Session 024134 · Attestation launch-readiness du `OPEN_DECISIONS_REGISTER` (18 citations `file:line` + 2 numériques d'en-tête re-prouvées) → 1 FIX de dérive latente : dé-figeage des 2 compteurs mobiles gelés dans le doc · 0 gate ajouté · 0 édition d'autorité **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre, roadmap intégralement livrée/gatée. Le `OPEN_DECISIONS_REGISTER.md` a été **promu découvrable hier** (session `021124`) mais son contenu — **18 citations `file:line` vers des sources d'autorité** (`CLAUDE.md`, directives datées, `pie_spec.json`, `gate.py`, journaux) — n'avait **jamais été re-vérifié indépendamment** contre les numéros de ligne courants. Un punch-list *launch-readiness* dont les ancres pointent à côté est trompeur. **Valeur honnête distincte** : attester chaque citation, et corriger toute dérive réelle du doc lui-même (le registre est **notre** doc worker, pas une source d'autorité — l'éditer pour l'exactitude est un travail worker légitime, distinct de *SURFACE-don't-rewrite* qui protège les autorités). **Attestation (read-only d'abord).** Les **18** citations re-prouvées exactes au numéro de ligne : - D-01 (PIE) : `pie_spec.json:24` (`contrats`→`legal/confotur`) · siblings `null` `:22,27,28,29,30` · `out/MANIFEST.json:24` · `README.md:48` · `out/pie_manifest.json:165-166` · `legal/confotur/README.md:8-17`. **Toutes exactes.** - D-02 (financement) : `DIRECTIVE_FINANCEMENT…:206-210` + `:275-279` (couche `## PRÉCISION` audit-IA) · `finlib/gate.py:142-145` (`_cond_validation_wag` teste encore `wag_validated_by`) · `financement_spec.json:157`. **Toutes exactes.** - D-03/D-04 (inventaire) : `AGENTS_EXISTING_ASSETS.md:102/127/128` · `CLAUDE.md:28` · `activity_log/2026-08-03.md:1245-1250`/`:1251-1253` · filesystem (`chat.py`, `projets_editor.py` absents). **Toutes exactes.** - D-05 (mobile) : `mobile_spec.json` (`a_confirmer`) · `out/app_config.json` (`bundleIdentifier`/`package` `null`) · `DIRECTIVE_MOBILE_STORES:15-16` (`com.otov7.app`). **Toutes exactes.** **FIX — dérive latente réelle (en-tête, ligne 9).** Le `> **Portée**` gelait **deux compteurs activement mobiles** : `33/33 gates` et `24 modules 4Big 100/100`. - `33` : le compte de `gate.needs` est passé **32→33 il y a 2 jours** (`b7ffb64`) ; le README **refuse délibérément** de le figer (« `gate.needs` dérivé de ci.yml, *sans liste en dur* », `README.md:28`). Le registre était le **seul** doc à le geler → à contre-courant de la discipline projet. - `24` : historique `21→22→24` (commentaires de `check_readme_claims.sh`) ; ce chiffre est **déjà gaté** (recomputé depuis `quality_report.json` totals pour le README + `audit_4big/README.md`). Le registre en était une **3ᵉ copie ungated** → dérive silencieuse au prochain module ajouté. - **Correction** (dé-figeage, pas gate #5) : reformulé en qualitatif — « tous les jobs de `gate.needs` passent · tous les modules 4Big au seuil `100/100`, verdict `PASS` » + **pointeur** vers les sources dérivées-et-gatées (`README ## État courant` + `quality_report.json`). Aligne le registre sur la philosophie *no-hardcoded-count* du README ; discipline single-source, **pas** un gate redondant. **Discipline.** Attestation confirmée AVANT tout edit (33/33 gates · 24 modules `100/100`/`PASS` re-dérivés : `gate.needs`→33 · `quality_report.json` totals→`{modules:24, pass:24, min/max:100, verdict:PASS}`). **0 édition d'autorité** (`CLAUDE.md`, directive, `pie_spec.json`, `gate.py` intouchés) · **0 gate ajouté** (#5) · **0 chiffre inventé** (#6, on **retire** deux chiffres, on n'en ajoute aucun) · **1 seule ligne** du registre modifiée. Items D-01..D-05 **inchangés** (la provenance « consolidation » reste `014119` — ce reword ne touche aucun item). **Vérifications.** `grep` compteurs `N/N`/`N modules` résiduels dans le registre → **aucun (clean)**. `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. Aucune commande VPS (#8) · aucun `git clean`. ## Session 021124 · `OPEN_DECISIONS_REGISTER` rendu découvrable — indexé dans la Navigation README + pointeur « État courant » (le punch-list launch-readiness n'était référencé nulle part) → 2 éditions README, 0 édition d'autorité **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre, roadmap intégralement livrée/gatée. La session `014119` (juste avant) a créé `05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` — une punch-list *launch-readiness* consolidée « pour que Michel la trouve ». **Mais elle ne la référençait de nulle part** : `grep -rl OPEN_DECISIONS_REGISTER --include=*.md` ne renvoyait **que** la ligne du journal `014119`. Le README `## Navigation` indexe pourtant son sibling `GAP_ANALYSIS_SPRINT1.md` ligne 24. Un punch-list que personne ne peut atteindre est de valeur nulle : **fermer cette boucle de découvrabilité** est la valeur honnête distincte du jour (ni gate redondant #5, ni re-surfaçage — le registre consolide déjà). **Vérification AVANT promotion** (anti-invention #6 · on ne met pas en avant un doc non re-vérifié). Re-prouvé indépendamment les claims-clés du registre au filesystem/source : - D-01 : `pie_spec.json:24` attribue bien `contrats` (Promesa · Fideicomiso · HOA) à `legal/confotur` — RÉEL. - D-03 : `CLAUDE.md:28` liste `chat.py` ; `/opt/oto/otoia/capabilities/chat.py` **absent** — RÉEL. - D-04 : `AGENTS_EXISTING_ASSETS.md:127` « projets_editor.py API en place » ; **aucun** `projets_editor.py` sous `/opt/oto/config` — RÉEL (claim runtime VPS non réfutable, #8). Registre jugé fidèle → promotion légitime. **Éditions (2, README seul).** 1. `## Navigation` — nouvelle ligne juste après `GAP_ANALYSIS_SPRINT1.md`, pointant le registre (description : punch-list décisions produit ouvertes qui appartiennent à Michel · sourcée, ne tranche pas). 2. `## État courant (sourcé)` — bullet « Décisions produit ouvertes » : *CI verte ≠ zéro décision en attente* → pointe le registre. **Sans compte chiffré en dur** (le nombre d'items dériverait · #6) : pointeur qualitatif uniquement. **Discipline.** *SURFACE, don't rewrite* : **0 édition** de `CLAUDE.md`, d'une directive datée, d'un `pie_spec.json`/`gate.py`, ni du registre lui-même. Aucun gate ajouté (#5). Les 2 lignes README sont de la prose de navigation (pas de claim numérique gaté). **Vérifications.** `ci/check_readme_claims.sh` → **vert** (chiffres README inchangés, fidèles). `ci/check_docs.sh` → **vert** (nouveaux liens internes valides). `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. **0 chiffre inventé (#6) · 3 claims re-prouvés avant promotion · 0 édition d'autorité · aucune commande VPS (#8) · aucun `git clean`.** ## Session 014119 · Registre des décisions ouvertes — consolidation sourcée des arbitrages produit surfacés au fil de l'eau (Sprint 8 launch-readiness) → 1 livrable-doc neuf · 0 édition d'autorité **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Roadmap intégralement livrée/gatée. Les **6 dernières sessions** (`200738` 08-03 → `011114` 08-05) ont saturé l'audit read-only (inter-artefacts, dérive free-text, attestation stakeholder, 8ᵉ gate mobile). Reproduire une 7ᵉ passe read-only aurait été à faible valeur / redondant (#5). **Valeur honnête distincte du jour** : les items *réellement ouverts* (arbitrages de périmètre produit + dépendances runtime hors dépôt) sont **éparpillés** dans les journaux de session et n'existaient **nulle part de façon consolidée et découvrable** pour Michel — un vrai trou de *launch-readiness* Sprint 8. J'ai créé `05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` : une punch-list unique, **100 % sourcée**, qui **regroupe** (ne tranche pas, ne réécrit aucune source d'autorité). **Vérification indépendante de CHAQUE signal AVANT consolidation** (anti-invention #6 · deux Explore parallèles + spot-check manuel). Aucun item repris de mémoire sans re-preuve : - **D-01 PIE `contrats`→`legal/confotur`** : REAL. `pie/manifest/pie_spec.json:24` attribue `contrats` (Promesa · Fideicomiso · HOA) à `legal/confotur` ; or ce module ne produit **que** le DocType demande CONFOTUR (`legal/confotur/README.md:8-17`). Incohérence interne confirmée : tous les autres downstreams sans générateur portent `"module": null` (`pie_spec.json:22,27-30`), `contrats` est le seul attribué à un module qui ne le génère pas. Arbitrage de périmètre. - **D-02 Financement condition #4 WAG→audit IA** : REAL. Directive `## PRÉCISION` (`DIRECTIVE_FINANCEMENT_..._20260803.md:275-279`) supersede `wag_validated_by` par `audit.decision=='APPROVED'` signé ; le module ship encore l'ancienne couche (`finlib/gate.py:142-145` + `financement_spec.json:157`). Dépend d'un agent auditeur hors dépôt. - **D-03 `chat.py` absent** / **D-04 `projets_editor.py` absent** : REAL, filesystem confirmé (ni `.py` ni `.pyc` sous `/opt/oto`), **déjà surfacés** session `20260803_133718` (`2026-08-03.md:1245-1253`) → registre = **pointeur** anti-doublon (#5), pas re-surfaçage. - **D-05 bundle id mobile** : soupçon INVALIDÉ → 🟢 VÉRIFIÉ-SANS-SUITE. Le renommage OTOV7→« OTO Enterprise OS » ne touche pas le bundle : `bundleIdentifier`/`package` quarantinés `null · a_confirmer`, découplés du nom d'app (`mobile_spec.json`, `out/app_config.json`). Consigné pour ne pas ré-ouvrir. Faux-positif écarté (#6). **Discipline.** *SURFACE, don't rewrite* respecté : **0 édition** de `CLAUDE.md`, d'une directive datée, ou d'un `pie_spec.json`/`gate.py` (les toucher serait invention de périmètre ou réécriture d'autorité). Le registre est un doc-pointeur volontairement non-gaté (chaque ligne cite sa source file:line, contenu 100 % dérivé — faible surface de dérive), au même titre que `daily_reports`. **Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier gaté touché). **0 chiffre inventé (#6) · 1 faux-positif écarté (bundle id) · 4 signaux re-prouvés indépendamment avant consolidation · 0 édition d'autorité · aucune commande VPS (#8) · aucun `git clean`.** ## Session 001109 · Nouveau gate `check-mobile-workflow` — la structure de `mobile-build.yml` devient opposable au merge (surface mutation-testée UNGATED, 1er contrôle) → 1 gate ajouté + câblé · 32→33 jobs **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0 SKIP**, arbre propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée (8 sprints). Les 4 sessions du 08-04 (`221055`, `224101`, `231101`, `234104`) avaient saturé les surfaces d'audit read-only (inter-artefacts, dérive non-gatée, attestation stakeholder) puis **durci `mobile-build.yml`** (pattern preflight-output, portable GitHub/Gitea). Cette dernière édition a laissé une **queue** : son propre log notait « Aucun gate ne linte la structure de `mobile-build.yml` (`check-ci-integrity` ne verrouille QUE `ci.yml`) ». Autrement dit le correctif de portabilité qu'elle venait de poser **pouvait régresser en silence** — exactement la classe « marche dans ma tête, casse le jour où on en a besoin » (mémoire [[mobile-build-secrets-if-gating]] : « workflow HORS `gate.needs` → `run_ci` won't catch a regression »). La valeur honnête du jour : **fermer cette queue** en rendant la structure de `mobile-build.yml` opposable au merge. NON redondant (#5) : c'est le **premier** contrôle de ce fichier. **Preuve d'absence de couverture AVANT d'ajouter le gate** (discipline [[verify-uncovered-before-gating]]). Mutation : réintroduire l'exact anti-pattern corrigé en `234104` — `secrets.EAS_TOKEN != ''` dans un `if:` au niveau job. Résultat : **7/7 gates statiques restent VERTS**. Surface confirmée non gatée (ni `check_ci_integrity` — scope `CI=ci.yml` — ni `validate_json` — scope `.json`). Fichier byte-restauré aussitôt (`cp` depuis backup tmp · jamais `git clean`). **Livrable : `ci/check_mobile_workflow.sh` (8ᵉ gate statique) + câblage.** Le gate lit des **FAITS dans le fichier** (jamais une liste à la main · #6), 4 invariants : - **MOB-1 · bien-formé** — existe · `name:`/`on:`/`jobs:` + 3 jobs (`preflight`, `build-ios`, `build-android`) ; parse YAML strict **exigé si PyYAML dispo**, sinon awk seul (runner sans pip → best-effort, jamais un faux vert : MOB-2/3/4 en sont indépendants). - **MOB-2 · portabilité** — **0** `secrets.` dans un `if:` de **job** (`^ if:`). Le contexte `secrets` n'est pas exposé dans `jobs..if` (table d'availability GitHub Actions) → sur `act`/Gitea il s'évaluerait vide → **SKIP silencieux**. C'est la régression `234104` verrouillée. - **MOB-3 · activation différée (#6/#8)** — chaque job de build (`run: eas build`) est gaté sur `needs.preflight.outputs.has_token` → SKIP tant que le token absent, donc le workflow ne rend jamais le CI rouge avant activation Michel. - **MOB-4 · contrat d'outputs** — `preflight` déclare `has_token`/`has_repo`, chaque `needs.preflight.outputs.` référencé est **déclaré** (0 dangling) et chaque output déclaré est **alimenté** par un `echo …>>$GITHUB_OUTPUT`. **Bug corrigé pendant l'écriture (auto-vérification).** 1er jet : `preflight` était classé « job de build » car un **commentaire** `# … eas build …` (l.108, entre deux jobs) était attribué au job précédent par le parseur awk. Corrigé en exigeant `run:[[:space:]]*eas build` (commande réelle, pas une mention). Après correctif : gate **VERT** sur le fichier propre. **Mutation-testing du gate lui-même (5 mutations · toutes ROUGES, fichier propre re-VERT).** (1) YAML cassé (indent) · (2) `secrets.` en `if:` de job · (3) `if:` du job `build-ios` supprimé · (4) output fantôme référencé (`has_tokenX`) · (5) output `has_repo` non alimenté. Chaque mutation → `exit 1` ; `cp` backup → gate re-VERT. **Câblage (garde `check-ci-integrity` INV-A/B vert).** Job `check-mobile-workflow` ajouté à `ci.yml` (steps `bash ci/check_mobile_workflow.sh`) **et** à `gate.needs`. `check_ci_integrity` reconnaît le nouveau `ci/*.sh` comme **câblé** (lancé par un job du gate + il source `lib.sh`) → INV-A (gate.needs == tous jobs non-manuels) et INV-B (tout `ci/*.sh` câblé) **restent verts**. `run_ci.sh` (dérivé de `gate.needs`, sans liste en dur) l'inclut **automatiquement** → passage **32→33 jobs (8 gates statiques + 25 suites)**, zéro édition de `run_ci.sh` (#6, exactitude structurelle). **Doc (traçabilité, pas de dérive).** `ci/README.md` : ligne §1 pour `check-mobile-workflow`, sous-section §2 détaillée (4 invariants + les 5 mutations), et nuance ajoutée à « Second workflow » (les **jobs** restent hors gate de merge, mais la **structure du fichier** est désormais gatée). Convention [[ci-readme-table-detail-in-section2]] respectée : ligne §1 courte, détail en §2. Les mentions historiques « 7 gates re-verts » (§2, records de mutations passées) restent exactes en **passé** ; le nouveau record dit **8ᵉ gate**. **Vérifications.** `bash ci/check_mobile_workflow.sh` → VERT (4 invariants) ; 5 mutations → ROUGE ; `python3 yaml.safe_load` sur `ci.yml` → valide ; `check_ci_integrity` → VERT (câblage intègre) ; `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. **1 gate ajouté (NON redondant : 1re couverture d'un fichier mutation-testé ungated · pas un doublon #5) · 1 job CI câblé · 0 nouveau module · 0 chiffre inventé (#6) · 0 édition de production hors CI · aucune commande VPS (#8) · aucun `git clean`.** --- ## Session 004112 · Currency du canal stakeholder `daily_reports` au jalon 33 jobs + réattestation indépendante → CLEAN · 0 édition de production **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Roadmap intégralement livrée+gatée. La session `001109` (même jour, plus tôt) a fait franchir au gate de merge le cap **32 → 33 jobs** (8ᵉ gate statique `check-mobile-workflow`). Or le canal roadmap-facing `daily_reports/` s'arrêtait au `2026-08-04` en affichant encore « **32 PASS** ». Les surfaces de dérive read-only étant saturées (re-scan = redondant #5), la valeur honnête du jour est de **rendre le canal stakeholder fidèle au point courant** — discipline [[two-logging-channels]] : garder les deux canaux, chaque figure sourcée d'un artefact commité. **Vérification préalable — pas de dérive présent-tense « 32 » résiduelle.** Balayage des surfaces present-tense (`README.md`, `ci/README.md`) : **aucun** décompte de jobs figé en dur (le nombre est **dérivé à l'exécution** par `run_ci.sh --list`, jamais retranscrit). Les nombreux « 32 » des logs/daily_reports antérieurs sont des **snapshots datés** (vrais à l'époque) → classe *historical-repro = KEEP* ([[prose-facts-vs-numeric-drift]]), on n'y touche pas. Le « 7 gates statiques » de `ci/README.md` l.49 décrit l'état **avant** l'ajout du 8ᵉ gate (narratif du mutation-test préalable) → exact en passé, KEEP. **Livrable : `05_deliverables_mvp/daily_reports/2026-08-05.md`.** Snapshot stakeholder consignant le jalon 33 jobs (= **8 gates statiques + 25 suites**) + réattestation. Chaque chiffre **recomputé `python3` indépendamment** des artefacts commités (aucune valeur saisie à la main · #6), tous concordants avec la table : - `quality_report.json` : **24/24 modules == 100** · `coverage.ok=true` · ci==registry==24 · verdict `PASS`. - `regression_run.json.totals` : **24 suites · 624 ran · 624 passed · 0/0/0** · verdict `PASS`. - `acceptance_matrix.json` : **15/15 `status=in_repo`** · verdict `True` · **`ci_job` ×36** (niché `matrix[].modules[]`). - Structure CI : `run_ci.sh --list` = **33 jobs = 8 statiques + 25 suites** · `ls ci/*.sh` (hors `lib.sh`) = **8**. **Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — livrable prose, hors `check_artifacts`/`audit_4big`). Liens Markdown du rapport résolus depuis `daily_reports/` : `../qa/…` (artefacts), `../../run_ci.sh`, `../../.gitea/…`, `../../ci/…` → `check_docs` VERT. **0 chiffre inventé (#6) · 0 édition de production · aucune commande VPS (#8) · aucun `git clean`.** --- ## Session 011114 · Double sweep read-only sur les DEUX classes de dérive free-text ungated (docstring↔code des 22 générateurs · inventaire↔filesystem) → CLEAN · 0 édition de production · faux « nouveau finding » écarté (anti-invention #6) **Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **33 PASS · 0 FAIL · 0 SKIP**, arbre propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée+gatée (8 sprints · 7 critères de succès ✅). Les deux sessions du jour ont déjà (a) ajouté le 8ᵉ gate statique `check-mobile-workflow` (`001109`, 32→33 jobs) et (b) mis le canal stakeholder `daily_reports` au jalon 33 jobs avec réattestation des 4 chiffres (`004112`). Les surfaces d'audit inter-artefacts / dérive gatée / attestation sont **saturées** (re-scan = redondant #5). La valeur honnête restante : re-vérifier les **deux classes de dérive free-text NON gatées** que la mémoire marque comme récurrentes/non-fermées, avant d'envisager quoi que ce soit — discipline [[verify-uncovered-before-gating]] (prouver l'état AVANT d'agir) + anti-invention #6. **Sweep 1 — docstring↔code des 22 générateurs ([[docstring-vs-code-drift]], classe RECURRING/not-closed).** Pour chacun des 22 `*_gen.py` de `05_deliverables_mvp/*` : comparaison du **docstring/en-tête** (inputs annoncés · fichiers `out/` produits · fallbacks) au **code réel** (argparse · `open(...,"w")` / `json.dump` / `write_text` · branches de fallback). Résultat : **22/22 CLEAN** — chaque docstring décrit exactement les sous-commandes, les inputs et les artefacts écrits. Spot-check manuel des deux générateurs multi-sorties les plus risqués (au-delà de la lecture agent) : - `demo/scenarios/demo_scenario_gen.py` — docstring l.20/l.32 annonce `run_sheet.json` + `run_sheet.md` + `MANIFEST.json` → code écrit bien les 3 (l.197-199 JSON, l.203-204 `.md`). ✓ - `faisabilite/bancable/bancable_gen.py` — docstring `50_financier_bancable/{fr,en,es}.md` → écrits l.171-173. ✓ Aucune régression du type « fallback §5.3 fantôme » (publiciste) ni « output omis » (demo run_sheet.md) qui avaient été corrigés antérieurement : la classe reste propre. **Sweep 2 — inventaire `AGENTS_EXISTING_ASSETS.md` ↔ filesystem `/opt/oto` ([[inventory-vs-filesystem-drift]]).** Extraction de tous les chemins `/opt/oto/*` cités (38 uniques), classification **SRC-OK / PYC-ONLY / MISS** (avec check `__pycache__`) : - **8 PYC-ONLY** (`oto_agent_faisabilite`, `oto_orchestrateur_faisabilite`, `oto_module_{etudes_faisabilite_real,execution_faisabilite,faisabilites_pages,faisabilites_viewer,mobile_api,plans_ifc}`) → **tous** déjà annotés « (compilé pyc) » dans le doc : convention interne **cohérente**, 0 dérive. - **2 MISS** : `otoia/capabilities/chat.py` (absent vs CLAUDE.md §Architecture cible qui le liste) et `config/projets_editor.py` (§13, « API GET/POST déjà en place » — sibling `projets_config.json` présent, lui). **Vérification anti-invention #6 AVANT de flaguer :** `grep -rn` interne → **les deux MISS sont DÉJÀ surfacés à Michel** par la session `133718` du **2026-08-03** (log l.1245/1284/1289-1295 : claims VPS-externes / non-réfutables laissés ouverts, chat.py ≠ le module chatbot-lead distinct). Donc **PAS un nouveau finding** — re-signaler serait redondant (#5). Le reste (containers Docker, Speckle, systemd, GSC) est runtime, non réfutable depuis le dépôt → hors périmètre worker (#8). **SURFACE, don't rewrite** respecté : 0 édition de l'inventaire (le fichier peut viser un runtime container-side ; un « fix » unilatéral serait une invention). **Conclusion honnête.** Les deux seules classes de dérive free-text ungated sont **stables et propres** à ce jalon ; la dimension chiffres-README est densément gatée (`check_readme_claims` = 27 claims / 16 modules + fiches). Rien à corriger, **aucun gate à ajouter** (ajouter serait un doublon #5 ; par ailleurs un gate lisant `/opt/oto` serait non-portable — le runner Gitea n'a pas ce filesystem, cf. [[ci-gate-verification-method]]). Session **read-only** volontaire, dans la lignée des attestations `234104`/`004112`, mais sur un **axe distinct** (les 2 classes free-text, pas les 4 chiffres stakeholder ni le cross-artefact). **Vérifications.** `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier gaté touché). **0 chiffre inventé (#6) · faux nouveau-finding écarté par grep interne · 0 édition de production · 0 gate ajouté (anti-redondance #5) · aucune commande VPS (#8) · aucun `git clean`.**