From c5ac020270c3b1183467477e3a5918dc92fe5733 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Fri, 31 Jul 2026 15:07:06 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20L?= =?UTF-8?q?75=20=C2=B7=20Domaine=20Mobile=20(2e=20surface)=20:=20la=20lign?= =?UTF-8?q?e=20r=C3=A9cap=20du=20README=20de=20l'app=20Expo=20=C3=89NUM?= =?UTF-8?q?=C3=89RAIT=20=C3=80=20LA=20MAIN=20ses=205=20onglets=20=C2=AB=20?= =?UTF-8?q?(Ventes/Construction/Achat/Compta/Direction)=20=C2=BB=20sans=20?= =?UTF-8?q?AUCUN=20gate=20d'IDENTIT=C3=89.=20Le=20bloc=20mobile=20existant?= =?UTF-8?q?=20recompute=20le=20QUADRUPLET=20de=20la=20ligne=20(onglets=20?= =?UTF-8?q?=C2=B7=20r=C3=B4les=20couverts=20=C2=B7=20langues=20=C2=B7=20id?= =?UTF-8?q?entifiants=20a=5Fconfirmer)=20depuis=20MANIFEST.counts,=20MAIS?= =?UTF-8?q?=20son=20patron=20du=20compte=20matche=20la=20parenth=C3=A8se?= =?UTF-8?q?=20par=20\([^)]*\)=20et=20en=20IGNORE=20le=20contenu=20?= =?UTF-8?q?=E2=80=94=20surface=20data-derived=20distincte=20du=20M=C3=8AME?= =?UTF-8?q?=20README,=20m=C3=AAme=20ligne.=20Cette=20liste=20d=C3=A9rive?= =?UTF-8?q?=20de=20mobile/app=5Fconfig/out/role=5Fnavigation.json=20(byte-?= =?UTF-8?q?gat=C3=A9=20par=20check=5Fartifacts)=20:=20un=20onglet=20par=20?= =?UTF-8?q?portail=20m=C3=A9tier=20du=20contrat=20RBAC=20(bijection=201=20?= =?UTF-8?q?onglet=20=E2=87=94=201=20portail,=20plateforme=20exclu),=20chaq?= =?UTF-8?q?ue=20entr=C3=A9e=20=3D=20son=20label=20=C2=AB=20OTO=20=20?= =?UTF-8?q?=C2=BB=20;=20nom=20court=20=C3=A9num=C3=A9r=C3=A9=20=3D=20label?= =?UTF-8?q?=20sans=20le=20pr=C3=A9fixe=20=C2=AB=20OTO=20=C2=BB.=20Le=20blo?= =?UTF-8?q?c=20mobile=20ne=20gate=20QUE=20le=20COMPTE=20(=C2=AB=205=20ongl?= =?UTF-8?q?ets=20=C2=BB)=20:=20un=20=C3=89CHANGE=20(Ventes=E2=86=92Marketi?= =?UTF-8?q?ng)=20laisse=20le=20compte=20=C3=A0=205=20=E2=80=94=20le=20comp?= =?UTF-8?q?teur=20reste=20AVEUGLE=20=E2=80=94=20pendant=20que=20l'artefact?= =?UTF-8?q?=20dit=20autre=20chose=20;=20un=20RETRAIT=20(Compta=20absent)?= =?UTF-8?q?=20fait=20tomber=20l'=C3=A9num=C3=A9ration=20=C3=A0=204=20tandi?= =?UTF-8?q?s=20que=20=C2=AB=205=20onglets=20=C2=BB=20reste=20juste=20;=20u?= =?UTF-8?q?n=20R=C3=89ORDRE=20passe=20aussi=20=E2=80=94=20=C2=AB=20vert=20?= =?UTF-8?q?trompeur=20=C2=BB=20qu'aucune=20suite=20tests/=20(qui=20teste?= =?UTF-8?q?=20des=20FONCTIONS,=20pas=20la=20prose)=20n'attrape=20=E2=86=92?= =?UTF-8?q?=20nouveau=20bloc=20dans=20check=5Freadme=5Fclaims=20recomputan?= =?UTF-8?q?t=20la=20LISTE=20ORDONN=C3=89E=20des=20noms=20courts=20depuis?= =?UTF-8?q?=20role=5Fnavigation=20(z=C3=A9ro=20duplication=20de=20la=20nav?= =?UTF-8?q?igation=20du=20g=C3=A9n=C3=A9rateur=20#6)=20et=20exigeant=20que?= =?UTF-8?q?=20la=20parenth=C3=A8se=20l'=C3=A9num=C3=A8re=20EXACTEMENT.=20C?= =?UTF-8?q?ontr=C3=B4le=20par=20ENSEMBLE=20(absent=20ET=20en=20trop=20mord?= =?UTF-8?q?us=20via=20set-diff),=20puis=20ORDRE.=20M=C3=AAme=20patron=20qu?= =?UTF-8?q?e=20l'=C3=A9num=C3=A9ration=20des=20confirmations=20DevOps,=20l?= =?UTF-8?q?a=20carte=20de=20renormalisation=20par=20arch=C3=A9type=204Big?= =?UTF-8?q?=20ou=20la=20table=20=C2=AB=20Verdict=20courant=20=C2=BB=20de?= =?UTF-8?q?=20l'audit=205D.=20Coh=C3=A9rence=20crois=C3=A9e=20en=20bonus?= =?UTF-8?q?=20:=20len(role=5Fnavigation)=20=3D=3D=20MANIFEST.counts.onglet?= =?UTF-8?q?s=20=3D=3D=20MANIFEST.counts.portails=20(la=20bijection=20du=20?= =?UTF-8?q?g=C3=A9n=C3=A9rateur)=20=E2=80=94=20mord=20un=20compteur=20du?= =?UTF-8?q?=20manifeste=20d=C3=A9synchronis=C3=A9=20de=20sa=20navigation.?= =?UTF-8?q?=20Un=20claim=20absent=20=C3=A9choue=20AUSSI=20(4=20morsures=20?= =?UTF-8?q?v=C3=A9rifi=C3=A9es=20:=20=C3=A9change=20Ventes=E2=86=92Marketi?= =?UTF-8?q?ng=20capt=C3=A9=20l=C3=A0=20o=C3=B9=20le=20compte=20reste=205?= =?UTF-8?q?=20=C2=B7=20retrait=20Compta=20capt=C3=A9=20l=C3=A0=20o=C3=B9?= =?UTF-8?q?=20=C2=AB=205=20onglets=20=C2=BB=20reste=20juste=20=C2=B7=20r?= =?UTF-8?q?=C3=A9ordre=20capt=C3=A9=20=C2=AB=20m=C3=AAme=20ensemble,=20ord?= =?UTF-8?q?re=20=E2=89=A0=20=C2=BB=20=C2=B7=20=C3=A9num=C3=A9ration=20supp?= =?UTF-8?q?rim=C3=A9e=20=3D=20INTROUVABLE=20;=20restaur=C3=A9=20=3D=20gree?= =?UTF-8?q?n=20:=20Ventes/Construction/Achat/Compta/Direction=20=3D=3D=20r?= =?UTF-8?q?ole=5Fnavigation=20=C2=B7=20bijection=205=3D=3Donglets=3D=3Dpor?= =?UTF-8?q?tails).=20=C3=89tat=20courant=20:=20aucun=20onglet=20p=C3=A9rim?= =?UTF-8?q?=C3=A9=20(anti-invention=20#6,=20rien=20=C3=A0=20r=C3=A9=C3=A9c?= =?UTF-8?q?rire)=20=E2=80=94=20le=20d=C3=A9faut=20est=20la=20surface=20ung?= =?UTF-8?q?ated.=20ci/README.md=20(table=20+=20d=C3=A9tail)=20mis=20=C3=A0?= =?UTF-8?q?=20jour=20=C2=B7=207=20gates=20re-verts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-07-31.md | 60 +++++++++++++++++++++++++++++++++ ci/README.md | 15 +++++++-- ci/check_readme_claims.sh | 62 +++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 1cb5c98..d04aa75 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,65 @@ # Activity Log · 2026-07-31 · Claude Code DTP +## Session `20260731_150110` · Buffer S8 · Domaine Mobile (2e surface) : la ligne récap du README app Expo **énumérait** À LA MAIN ses **5 onglets** (`(Ventes/Construction/Achat/Compta/Direction)`) sans gate d'IDENTITÉ — le bloc mobile existant ne recompute que le COMPTE (« 5 onglets ») et son patron matche la parenthèse par `\([^)]*\)` en **ignorant son contenu**, aveugle à un **échange / retrait / réordre** de nom. + +**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive (CLAUDE.md #6). Le bloc `mobile` gaté +précédemment recompute le **quadruplet** de la ligne récap (`onglets` · `rôles` +couverts · `langues` · `identifiants a_confirmer`) depuis `MANIFEST.counts`. Restait, +dans la **même ligne du même README**, une surface data-derived distincte : la +**parenthèse qui énumère les 5 onglets** — matchée par le patron du compte mais +dont le **contenu est ignoré**. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/mobile/app_config/README.md:66` — « **5 onglets** + (Ventes/Construction/Achat/Compta/Direction) · … ». La parenthèse est une **liste + ordonnée data-derived** ; le patron du compte (`\*\*(\d+) onglets\*\* \([^)]*\)`) + la matche mais IGNORE son contenu. +- Source faisant autorité : `mobile/app_config/out/role_navigation.json` (byte-gaté + par `check_artifacts`) — un onglet par portail métier du contrat RBAC (bijection + 1 onglet ⇔ 1 portail · `plateforme` exclu), chaque entrée portant `portail` (id) + + `label` « OTO ». Nom court énuméré = le `label` sans le préfixe « OTO ». +- Le bloc mobile ne gate QUE le **compte** (« 5 onglets »). Un **échange** + (`Ventes → Marketing`), un **retrait** (`Compta` absent — l'énumération tombe à 4 + tandis que « **5 onglets** » reste juste, aveugle au compte) ou un **réordre** + laisse le compte à 5 pendant que l'artefact dit autre chose. « Vert trompeur » + qu'aucune suite `tests/` (qui teste des FONCTIONS, pas la prose) n'attrape. +- **État courant** : **aucun onglet périmé** — la liste des 5 recoupe l'artefact + exactement (anti-invention #6, rien à réécrire). Le défaut est la **surface ungated**. + +**Nouveau bloc dans `ci/check_readme_claims.sh`** : recompute la **liste ordonnée** +des noms courts depuis `role_navigation.json` (zéro duplication de la navigation du +générateur · #6) et exige que la parenthèse l'énumère **exactement** : contrôle par +**ENSEMBLE** (absent ET en trop mordus via set-diff), puis **ordre**. Même patron que +l'énumération des confirmations DevOps, la carte de renormalisation par archétype 4Big +ou la table « Verdict courant » de l'audit 5D. Cohérence croisée en bonus : +`len(role_navigation) == MANIFEST.counts.onglets == MANIFEST.counts.portails` (la +bijection du générateur) — mord un compteur du manifeste désynchronisé de sa navigation. + +### Preuve (4 morsures vérifiées + restauration verte) + +| Injection | Résultat | +|---|---| +| échange `Ventes → Marketing` (compte reste 5 · gate-compte aveugle) | ✗ énumère `[Marketing,…]` MAIS `role_navigation` `[Ventes,…]` · absents=`[Ventes]` · en trop=`[Marketing]` · exit 1 | +| retrait `Compta` (énumération=4 · « 5 onglets » reste juste) | ✗ énumère 4 MAIS `role_navigation` 5 · absents=`[Compta]` · exit 1 | +| réordre `Construction/Ventes/…` (même ensemble) | ✗ même ensemble, ordre ≠ · exit 1 | +| énumération supprimée (claim disparu) | ✗ énumération des onglets INTROUVABLE · exit 1 | +| restauré | ✓ `Ventes/Construction/Achat/Compta/Direction` == `role_navigation` · bijection 5==onglets==portails · exit 0 | + +- **`ci/README.md`** — surface ajoutée à la table récap du pipeline + paragraphe + descriptif (2ᵉ surface du même README mobile). + +Suite CI complète re-verte (7 gates) : `guard_constraints` · `validate_json` · +`check_docs` · `check_artifacts` · `check_regression` · `check_ci_integrity` · +`check_readme_claims`. + +**Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; +édition **hors** `05_deliverables_mvp` ⇒ 0 dérive d'artefact). + +**Auto-score 4Big** : 96/100. + +--- + ## Session `20260731_143104` · Buffer S8 · Domaine QA/Audit 5D (2e surface) : la **table « Verdict courant »** de l'audit de conformité énumérait À LA MAIN ses **4 open items** (`control → dimension → propriétaire`) sans gate d'IDENTITÉ — le bloc existant ne recompute que le COMPTE (« 4 à confirmer »), aveugle à un **échange** d'open item. **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement diff --git a/ci/README.md b/ci/README.md index 16d8f0e..c468e45 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -207,7 +207,18 @@ EAS/stores jamais fabriqués). Les quatre sont recomputés depuis `mobile/app_config/out/MANIFEST.json` (`counts.{onglets,roles_couverts,langues, identifiants_a_confirmer}`, byte-gaté par `check_artifacts`). Recomputer **les quatre** volets mord une dérive de n'importe lequel (ajouter un rôle/portail RBAC, -une langue, un credential store) là où un seul nombre resterait aveugle. +une langue, un credential store) là où un seul nombre resterait aveugle. **2ᵉ +surface du même README** : la même ligne récap **énumère** les 5 onglets +(« (Ventes/Construction/Achat/Compta/Direction) ») — le patron du compte matche +la parenthèse par `\([^)]*\)` mais en **ignore le contenu**. Cette liste est +data-derived : `mobile/app_config/out/role_navigation.json` (byte-gaté) porte un +onglet par portail métier (bijection 1 onglet ⇔ 1 portail), chaque entrée = son +`label` « OTO ». On recompute la **liste ordonnée** des noms courts et on +exige que la parenthèse l'énumère **exactement** : un **échange** (Ventes→ +Marketing), un **retrait** (Compta absent — l'énumération tombe à 4 tandis que +« **5 onglets** » reste juste) ou un **réordre** est mordu là où le seul compte +est aveugle. Cohérence croisée : `len(role_navigation) == counts.onglets == +counts.portails` — mord un compteur du manifeste désynchronisé de sa navigation. Même classe enfin sur le **DocType CONFOTUR Application** (livrable `legal/confotur`) : la ligne de synthèse du README du module (« **18 champs** (14 de donnée) · **4 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 8b5fa3f..219429d 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -886,6 +886,68 @@ if mob_exp is not None: bad(f"Mobile · {MOB_RD} — dit {'/'.join(m.groups())} MAIS MANIFEST dit " f"{'/'.join(mob_exp)} ({mob_src}) → régénérer le README") +# ============================================================================ +# Mobile (2e surface) · ÉNUMÉRATION des onglets de l'app Expo — même README, même +# ligne récap « **5 onglets** (Ventes/Construction/Achat/Compta/Direction) ». +# ---------------------------------------------------------------------------- +# Le bloc mobile ci-dessus recompute le COMPTE d'onglets (« 5 ») mais son patron +# ne mord QUE le nombre : la parenthèse qui ÉNUMÈRE les 5 portails est matchée par +# `\([^)]*\)` et son CONTENU est IGNORÉ. Or cette liste est DATA-DERIVED : le +# générateur app_config émet out/role_navigation.json (byte-gaté par +# check_artifacts ⇒ artefact faisant autorité) = un onglet par portail métier du +# contrat RBAC (bijection 1 onglet ⇔ 1 portail · `plateforme` exclu), chaque +# entrée portant `portail` (id) + `label` (« OTO »). Le nom court énuméré au +# README = le label sans le préfixe « OTO ». Un ÉCHANGE (Ventes→Marketing), un +# RETRAIT (Compta absent — l'énumération tombe à 4 tandis que « **5 onglets** » +# reste juste) ou un RÉORDRE ferait dériver la prose EN SILENCE pendant que +# l'artefact dit autre chose : « vert trompeur » aveugle au seul compte, qu'aucune +# suite tests/ (qui teste des FONCTIONS, pas la prose) n'attrape — même patron que +# l'énumération des confirmations DevOps, la carte de renormalisation par archétype +# 4Big ou la table « Verdict courant » de l'audit 5D. On RECOMPUTE ici la LISTE +# ORDONNÉE des noms courts depuis role_navigation.json (zéro duplication de la +# navigation du générateur) et on exige que la parenthèse l'énumère EXACTEMENT +# (absent ET en trop mordus, puis ordre). Cohérence croisée en bonus : +# len(role_navigation) == MANIFEST.counts.onglets == MANIFEST.counts.portails (la +# bijection du générateur) — mord un compteur du manifeste désynchronisé de sa +# propre liste de navigation. +# ============================================================================ +try: + mob_nav = load("mobile/app_config/out/role_navigation.json") + # nom court = label « OTO » sans le préfixe « OTO » (ce que le doc copie) + mob_names = [re.sub(r"^OTO\s+", "", e["label"]).strip() for e in mob_nav] + mob_names_c = load("mobile/app_config/out/MANIFEST.json")["counts"] +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Mobile · role_navigation/MANIFEST illisible/inattendu : {e}"); mob_names = None +if mob_names is not None: + # bijection : la liste de navigation == compteurs onglets ET portails du manifeste + n_nav = len(mob_nav) + if not (n_nav == mob_names_c.get("onglets") == mob_names_c.get("portails")): + bad(f"Mobile · bijection rompue : |role_navigation|={n_nav} MAIS " + f"MANIFEST onglets={mob_names_c.get('onglets')} " + f"portails={mob_names_c.get('portails')} → régénérer l'artefact") + else: + good(f"Mobile · bijection onglet⇔portail : {n_nav} == onglets == portails") + # énumération de la parenthèse : capturer le contenu entre ( ) qui suit « N onglets » + mob_enum_pat = r"\*\*\d+ onglets\*\* \(([^)]*)\)" + mob_txt2 = read_norm(MOB_RD) + if mob_txt2 is not None: + me = re.search(mob_enum_pat, mob_txt2) + if me is None: + bad(f"Mobile · {MOB_RD} — énumération des onglets INTROUVABLE " + f"(attendu {'/'.join(mob_names)})") + else: + got = [s.strip() for s in me.group(1).split("/") if s.strip()] + if got == mob_names: + good(f"Mobile · {MOB_RD} — énumération onglets {'/'.join(got)} " + f"== role_navigation") + else: + miss = [x for x in mob_names if x not in got] + extra = [x for x in got if x not in mob_names] + detail = (f" · absents={miss}" if miss else "") + \ + (f" · en trop={extra}" if extra else "") or " · même ensemble, ordre ≠" + bad(f"Mobile · {MOB_RD} — énumère {got} MAIS role_navigation dit " + f"{mob_names}{detail} → régénérer le README") + # ============================================================================ # Legal · DocType CONFOTUR Application (champs / sections / rôles / évènements de # dépôt) — même classe de « vert trompeur », transcrit À LA MAIN dans DEUX docs.