From 290bd2ec02b486ef67ebee4a20a9769f5fafb938 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 15:15:24 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20DevOps/CI=20:=20gate=20d'IDENTIT=C3=89=20des=20identif?= =?UTF-8?q?iants=20d'infrastructure=20VPS=20(IP=20153.75.250.214=20+=20con?= =?UTF-8?q?teneurs=20Docker=20erpnext-backend-1/erpnext-frontend-1)=20?= =?UTF-8?q?=E2=80=94=20recomput=C3=A9s=20de=20CLAUDE.md=20=C2=A7VPS=20?= =?UTF-8?q?=C2=B7=20direction=20copie=E2=86=92source=20sur=20toute=20doc?= =?UTF-8?q?=20.md=20du=20mandat=20(hors=20journaux)=20=C2=B7=20enveloppe?= =?UTF-8?q?=20conteneur=20D=C3=89RIV=C3=89E=20des=20stems=20service=20(bac?= =?UTF-8?q?kend/frontend)=20et=20non=20du=20pr=C3=A9fixe=20compose=20?= =?UTF-8?q?=E2=87=92=20un=20renommage=20erpnext-*=E2=86=92oto-*=20mord=20?= =?UTF-8?q?=C2=B7=20migration=20serveur=20cascade=20sur=20les=2010=20copie?= =?UTF-8?q?s?= 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-08-01.md | 74 +++++++++++++++++++++++ ci/README.md | 2 +- ci/check_readme_claims.sh | 110 ++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 97a8e3d..c26eddf 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -1634,3 +1634,77 @@ source constitutionnelle) ; restauré = green : renderer/test == #9/#10 (6 marqu édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la génération réelle des faisabilités reste côté BIM/Faisabilité). - **Auto-score 4Big** : 96/100. + +## Session `20260801_150344` · Buffer S8 · Domaine DevOps CI/CD : gate d'**IDENTITÉ des IDENTIFIANTS D'INFRASTRUCTURE VPS** (IP `153.75.250.214` + les DEUX conteneurs Docker `erpnext-backend-1`/`erpnext-frontend-1`) ancré sur **CLAUDE.md §VPS** — surface data-derived jusqu'ici **HORS de tout ancrage**. + +**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive d'ANCRAGE à CLAUDE.md (classe +[[claude-md-constant-anchor-gate]] + [[agent-fiche-role-attrs-ungated]], surface +**infra ~ endpoint RunPod**). **Même patron EXACT** que le gate « endpoint GPU +RunPod » (session 130325 : un identifiant d'infra hand-transcrit dans N docs, +recomputé de la source in-repo faisant autorité) — appliqué ici aux identifiants +**serveur** dont la source de vérité est **constitutionnelle** (CLAUDE.md §VPS). + +**Dérive silencieuse fermée** : CLAUDE.md §VPS FIXE l'IP du VPS `153.75.250.214` +et les 2 conteneurs Docker `erpnext-backend-1`/`erpnext-frontend-1`. Ces +identifiants sont RECOPIÉS À LA MAIN dans ~10 endroits, sans AUCUN ancrage : +- `README.md` (×2 · VPS hors périmètre + dépôt Gitea) · `02_master_prompt/ + MASTER_PROMPT_DTP_v2.md` (IP ×2 + les 2 conteneurs) · `AGENTS_EXISTING_ASSETS.md` + (IP + 2 conteneurs) · `03_agents/mobile/AGENT.md` (`erpnext-backend-1`, backend + cible de l'API) · `05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` (run-book + d'import) · `05_deliverables_mvp/GAP_ANALYSIS_SPRINT1.md` · `tests/README.md` + (base URL Playwright) · `ci/README.md` (instance Gitea). +- Aucun gate ne couvrait ce lien : `check_docs` ne voit qu'un lien Markdown, + `guard_constraints` qu'un terme interdit, `check_artifacts` qu'un `out/` — une IP + ni un nom de conteneur n'est aucun des trois. Aucune suite `tests/` (FONCTIONS + des modules, jamais la prose infra) ne l'attrape. +- Drift la plus réaliste, #6 : une **MIGRATION serveur** (nouvelle IP) ou un + **RENOMMAGE du projet compose** (`erpnext-*` → `oto-*`) — évènement ops COURANT : + CLAUDE.md §VPS se met à jour, les ~10 copies se PÉRIMENT en silence → un agent + câblerait l'API mobile / les tests e2e / le hand-off d'import vers un serveur + MORT ou un conteneur FANTÔME (l'invention même que #6 interdit). +- **État courant** : aucune copie périmée — l'unique IPv4 du corpus == canonique, + les seuls conteneurs cités ⊆ l'ensemble canonique (vérifié programmatiquement + AVANT édition). Le défaut est la **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « CLAUDE.md §VPS · IDENTITÉ +des IDENTIFIANTS D'INFRASTRUCTURE » avant `sys.exit`) : RE-DÉRIVE l'IP + l'ENSEMBLE +des conteneurs de CLAUDE.md §VPS (`^## VPS` → `IP ` + `Docker (…)`, jamais une +liste à la main · #6 ; garde « section/format introuvable » = ancre morte). +Direction copie→source sur `git ls-files '*.md'` (hors `CLAUDE.md` = source, hors +`05_activity_log/` + `daily_reports/` = journaux figés à ne pas réécrire) : tout +token **IPv4** DOIT == l'IP canonique ; tout token **conteneur** DOIT ∈ l'ensemble +canonique. Garde anti-évaporation (IP ET ≥1 conteneur restent cités quelque part). +Le write-path interdit `/var/www/html/static` reste du ressort de +`guard_constraints`, non de ce gate d'identité. + +**Piège de conception CORRIGÉ en vérif adversariale** : 1ʳᵉ version ancrait +l'enveloppe conteneur sur le préfixe LITTÉRAL `erpnext-[a-z]+-1` → un renommage de +projet compose dans une copie (`erpnext-backend-1` → `oto-backend-1`, le drift MÊME +qu'on vise) ne matchait plus le shape et **PASSAIT en silence** (M2 vert à tort). +Correctif : l'enveloppe est DÉRIVÉE des **stems de service** canoniques +(`backend`/`frontend`) via `[a-z0-9]+-(?:backend|frontend)-1`, PAS du préfixe → +`oto-backend-1` est vu comme fantôme. Le filtre source est lui aussi générique +(`[a-z0-9]+-[a-z]+-1`) pour que M4 (renommage DANS §VPS) cascade avec un message +utile plutôt qu'un « format modifié » crude. + +**5 morsures vérifiées** (mutations sur fichiers DONNÉES uniquement, restaurées par +`git checkout -- ` ciblé — **JAMAIS** `git clean`/`checkout` large avec le +gate décommité en cours, interdit absolu + incident process en mémoire) : (M1) IP +périmée dans `README.md` (serveur migré, copie non mise à jour) → stale · (M2) +conteneur `erpnext-backend-1→oto-backend-1` dans le master prompt (compose renommé) +→ fantôme (après correctif) · (M3) IP changée DANS CLAUDE.md §VPS → **cascade sur +les 5 docs citant l'IP** · (M4) conteneurs renommés DANS §VPS → **cascade fantôme +sur les 4 docs citant un conteneur** avec message utile · (M5) header `## VPS` +retiré → ancre morte. Restauré = green : 10 assertions VPS infra (5 IP + 5 +conteneur) · exit 0. **7 gates re-verts** (`check_readme_claims`/`check_artifacts`/ +`check_ci_integrity`/`check_docs`/`guard_constraints`/`check_regression`/ +`validate_json`). + +- `ci/README.md` (clause récap du pipeline `check-readme-claims` : sous-clause + « identité d'infrastructure VPS ») mise à jour — et désormais **elle-même couverte + par le gate** (elle gagne les tokens conteneur/IP canoniques, tous verts). +- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; + édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la migration + serveur réelle + le renommage compose restent côté DevOps/VPS production). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 7b33c55..a8641b5 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 + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **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`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis » + **promesse anti-invention #6** `taux_pct/source/a_confirmer` tout-`null` confrontée à `commission_plan.json` dans les deux sens, invariant #7 « aucun taux sans source », `MANIFEST.counts` recoupé) **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README **ET dans les 3 copies aval : `renderer.py::_CANONICAL_MARKERS` l'oracle d'enforcement de `check_canonical`, les 2 docstrings de `scorer.py`, et l'oracle du test `test_generator.py`**, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) **et** des tokens design canoniques `publiciste/branding` (**ancrés sur CLAUDE.md #4** : les 4 valeurs de la marque luxury `#0a0a12`/`#f0b429`/`Fraunces`/`Cormorant Garamond` recopiées à QUATRE endroits jamais gatés — (a) les CONSTANTES `lib/branding.py` `COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`, hors `out/` donc invisibles à check_artifacts, importées par le générateur & substituées dans le gabarit `{{COLOR_BG}}…` ; (b) le docstring du fichier ; (c) le README ; (d) le tuple d'oracle HARDCODÉ de `tests/test_publiciste.py` que le seul test existant assert sans jamais le comparer à CLAUDE.md — recomputés de la ligne #4 de `CLAUDE.md` puis exigés EXACTS : (a) constantes == #4 (hex insensible à la casse, fontes à l'exact), (b) docstring cite les 4, (c) README cite les 2 fontes, (d) oracle du test == #4 ; même patron que les paramètres canoniques `faisabilite/generator`, un changement de #4 mord SIMULTANÉMENT les 3 copies aval ; un claim absent échoue AUSSI) **et** de l'identité du RÔLE émetteur e-CF `fiscal/ecf_dgii` (3e surface du MÊME README : le seul rôle habilité à ÉMETTRE un e-CF — bullet `role_id` README:51-54 — nommé par `role_id` « `compta-fiscaliste-ecf` » = celui que TOUS les `emission_events` d'`ecf_plan.json` byte-gaté référencent, `erpnext_role_name` « _OTO Compta Fiscaliste eCF_ », et `portail` « `compta` » résolu du contrat `rbac_50_roles.json` ; recomputés des deux sources — zéro duplication — prose EXACTE exigée ; réaffecter l'émission à un autre rôle, le renommer, ou DÉPLACER ce rôle hors du portail `compta` (e-CF émis HORS Compta, séparation cassée) mord ; cohérences croisées : `emission_events` pointent UN SEUL rôle (fonction · non vide) · `role_id` unique au contrat · nom artefact == nom contrat · `portail == compta` ; un claim absent échoue AUSSI) **et** de la colonne « Job CI » des fiches agents `03_agents/*/AGENT.md` (chaque token `*-tests` cité entre backticks — le job Gitea Actions qui exécute la suite du module — recomputé de la section `jobs:` de `.gitea/workflows/ci.yml`, jamais gaté avant : `check_ci_integrity` prouve que chaque job est CÂBLÉ dans `gate.needs` mais RIEN ne liait le NOM écrit dans la fiche au job réel ; on exige que chaque token de fiche DÉSIGNE un job existant — renommer un job dans ci.yml ou le mal recopier laisse la fiche pointer un job FANTÔME pendant que le CI reste vert ; direction fiche→ci.yml, ci.yml peut définir des jobs non cités mais une fiche ne peut citer un job absent ; la disparition de la colonne échoue AUSSI) **et** de la colonne « Entrée CLI » des fiches agents `03_agents/*/AGENT.md` (le jeu de verbes cité entre backticks — `publiciste.py parse\|validate\|generate\|run`, `faisabilite_gen.py score\|scaffold\|generate\|batch`, `rbac_apply_plan.py build\|validate`, … 16 cellules — recomputé **par AST** de l'ensemble ORDONNÉ des `subparsers.add_parser("")` du script réel, jamais gaté avant : les blocs amont couvrent les colonnes « Tests » et « Job CI » mais RIEN ne liait les VERBES écrits aux sous-commandes que le CLI EXPOSE ; script résolu par le chemin de module de la MÊME ligne de table + basename ; égalité EXACTE ensemble ET ordre exigée — renommer/ajouter/retirer/réordonner une sous-commande dans le `.py` (ou la mal recopier dans la fiche) laisse la fiche documenter une surface d'invocation FANTÔME pendant que le module tourne sous le vrai verbe ; direction fiche→CLI ; script introuvable ou disparition de la colonne échoue AUSSI) **et** de l'IDENTITÉ du DocType porteur `crm/dossier_vente` (le NOM `OTO Dossier Vente` — point d'attache du pipeline — recopié à la main dans le titre & la ligne « Nom du DocType » du README `dossier_vente`, la fiche `03_agents/crm/AGENT.md:25` et la fiche `03_agents/onapi_legal/AGENT.md:50`, jamais gaté sur le NOM (le bloc pipeline ne recompute que le COMPTE d'états) : recomputé du champ `name` de `out/doctype_oto_dossier_vente.json` byte-gaté, chaque prose exigée EXACTE + cohérences croisées `DocType.name == MANIFEST.doctype_name` & `== workflow.document_type` (l'attache porteur↔workflow — sinon le Workflow vise un DocType FANTÔME) ; renommer le DocType dans le spec reconstruit les 2 artefacts cohéremment mais périme la prose en silence ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ du nœud racine `Organization` du graphe schema.org SEO (**ancrée sur CLAUDE.md §Entités** : le nom de marque `Helios RD` (marque publique) + son `url` == base_url — le bloc composition amont ne gate que les `@type`/bijection, jamais l'IDENTITÉ de marque que Google indexe sous `vente.otov7.com` ; recomputés de `seo_schema_org.json` byte-gaté + `MANIFEST.base_url` + le `country_code` du spec : (a) `Organization.name` == marque re-dérivée de CLAUDE.md §Entités — renommer la marque dans le spec ou dans CLAUDE.md émet un JSON-LD public qui CONTREDIT le mandat tout en restant byte-vert (l'artefact se reconstruit fidèlement depuis un spec dérivé), même « vert trompeur » que les tokens branding ancrés sur #4 — (b) `Organization.url` == base_url & `@id` sous base_url, (c) chaque listing rattaché à CETTE Organization (`brand.@id`) sous base_url, (d) `addressCountry` UNIFORME == `country_code` du spec, (e) le spec DÉCLARE l'ancrage (`organization.source` cite CLAUDE.md)) **et** des entités porteuses du DocType CONFOTUR `legal/confotur` (**ancrées sur CLAUDE.md §Entités** : le champ Select `entite_porteuse` offre les 7 entités canoniques `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC`, dérivées via `options_source` de `confotur_spec.json[entites]` et byte-gatées pour la reproductibilité mais jamais ancrées au mandat — les blocs CONFOTUR amont ne gatent que le COMPTE de champs/rôles et la cross-cohérence des permissions ; recomputées des tokens en gras de la section « ## Entités » de CLAUDE.md : (a) options de l'artefact == entités CLAUDE.md ORDRE exact, (b) `spec[entites]` == CLAUDE.md l'INPUT ancré, (c) le spec DÉCLARE l'ancrage `entites_source` cite CLAUDE.md + chaque entité, (d) wiring intact `options_source == "entites"`, (e) README cite l'ancre CLAUDE.md + nomme chaque entité par nom complet OU 1er token — renommer une entité dans CLAUDE.md seul mord SIMULTANÉMENT artefact/spec/déclaration/README, l'ancre est vive ; un claim absent échoue AUSSI) **et** de la cross-cohérence PERMISSIONS CONFOTUR répliquée dans la FICHE AGENT `03_agents/onapi_legal/AGENT.md` (3e surface : la §Permissions de la fiche recopie la MÊME table role→portail→actions que le README du module — `ventes-confotur` (Ventes)→read/write/create/print · `legal-onapi` (Direction)→read/write/create · `legal-directeur` (Direction)→read/write/**submit**/report — sous une convention DIFFÉRENTE (` / ` espacé · gras `**submit**`), gatée par RIEN alors que le gate README ne voit QUE le README ; recomputées de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`, byte-gaté), gras/casse normalisés, set-diff par rôle + identité d'ensemble fiche⇔MANIFEST — promouvoir un rôle à `submit`, retirer une action, réaffecter un portail, renommer/ajouter un rôle fantôme dans la fiche mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de la persona & des capabilities OTOIA du Chat par portail `frontend/chat_otoia` (**ancrées sur CLAUDE.md §Architecture cible** : la persona `Amélie` / voix `multilingual_v2` + les 4 capabilities `aec.py`+`knowledge.py`+`prompt_engine.py`+`chat.py`, portées par `out/chat_mount.json` (5 mounts) & `out/MANIFEST.json` byte-gatés pour la reproductibilité mais jamais ancrées au mandat — le bloc chat_otoia amont ne gate que la COMPOSITION (portails métier · 1 block⇔1 mount) et le SEUL contrôle d'identité `tests/` a un ORACLE HARDCODÉ jamais comparé à CLAUDE.md ; recomputées de CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) » · « OTOIA capabilities : … ») : (a) `chat_spec.json` persona/capabilities == CLAUDE.md, (b) le spec DÉCLARE l'ancrage (`persona.source` + chaque `capability.source` citent CLAUDE.md), (c) CHACUN des 5 mounts + le MANIFEST portent persona/capabilities == CLAUDE.md, (d) le README cite l'ancre §Architecture cible + nomme persona + chaque capability, (e) l'ORACLE du test == CLAUDE.md — renommer la persona/voix ou renommer/retirer une capability dans le spec (ou dans CLAUDE.md) mord tout en restant byte-vert ET tests-verts, même « vert trompeur » que les tokens branding ancrés sur #4 ; un claim absent échoue AUSSI) **et** du SDK MAJEUR Expo de l'app mobile `mobile/app_config` (**ancré sur la roadmap Sprint 5 l.56** « Rebuild Expo 54 » : la version majeure `expoSdkMajor` que l'app cible — recopiée CINQ fois dans le README (`Expo 54`/`Expo SDK **54**`) — jamais gatée hors le quadruplet onglets/rôles/langues/identifiants ; recomputée du champ `expo.extra.expoSdkMajor` de `out/app_config.json` byte-gaté : (a) cohérence interne artefact ⇔ `MANIFEST.app.expo_sdk_major` ⇔ `mobile_spec.json[app].expo_sdk_major`, (b) ANCRAGE roadmap `Rebuild Expo ` == artefact (ancre vive : bumper le SDK dans l'artefact ou dans la roadmap sans l'autre mord), (c) le spec DÉCLARE l'ancrage (`expo_sdk_source` cite roadmap + Expo N), (d) TOUTE mention prose « Expo [SDK] N » == artefact (aucune périmée), (e) la ligne de source du tableau cite l'ancre `roadmap` ; un claim absent échoue AUSSI) **et** du jeu de DEVISES `moneda` de l'e-CF `fiscal/ecf_dgii` (**ancré sur CLAUDE.md #10** : les options `USD`/`DOP` du champ `TipoMoneda`, byte-gatées dans `out/ecf_plan.json` mais jamais ancrées au mandat — les blocs fiscaux amont gatent le FormaPago (Cardnet) et les états/champs émetteurs, aveugles au jeu de devises, et le bloc « Fiches #10 » ancre les fiches ERPNext/CRM, aveugle au module fiscal ; recomputées de la devise en gras de la contrainte #10 (`USD + DOP`, réutilise l'`exp["devises"]` déjà parsé · zéro duplication) : (a) `spec[moneda].options` == #10 ORDRE exact (mord ajout/retrait/réordonnancement), (b) l'artefact byte-gaté idem, (c) le spec DÉCLARE l'ancrage via un champ DISTINCT `options_source` — `source` restant legit. `null` (taux TipoCambio à confirmer · #8) — qui cite `CLAUDE.md #10` + chaque devise, (d) la prose README qui se réclame de #10 cite le jeu EXACT + toute mention `moneda`/`devise` d'une paire de devises concordante — étendre/renommer une devise dans #10 ET le spec de façon cohérente reste byte-vert tout en faisant émettre un `TipoMoneda` hors mandat, même « vert trompeur » que la marque SEO §Entités ou les tokens branding #4 ; un claim absent échoue AUSSI) **et** de l'IMPUTABILITÉ des confirmations préalables VPS `devops/deploy_runbook` (3e surface : la ligne « Confirmations préalables VPS » du README annote chaque confirmation d'un couple `nom` (**owner** · **réf-contrôle**) — ex. `taux_commission` (Direction · audit_5d D1.1) — data-derived des `confirmations` de `out/deploy_runbook.json` byte-gaté CROISÉES avec le rapport `qa/audit_5d` byte-gaté ; les blocs DevOps amont ne gatent que le COMPTE et l'ENSEMBLE des noms, aveugles à QUI porte chaque confirmation et à QUEL contrôle la trace ; cross-anchor à trois : owner run-book ⊆ owner du contrôle audit_5d cité (tolère le suffixe « / UAF ») · owner README ⊆ owner run-book (tolère l'abréviation « Fiscaliste eCF » ⊂ « Compta Fiscaliste eCF ») · réf README == contrôle réel présent dans audit_5d ET dans la source — réaffecter un owner, misattribuer le contrôle (`D1.1`→`D2.1`) ou citer un contrôle FANTÔME (`D9.9`) mord ; une confirmation tracée DOIT rester annotée, une annotation d'un nom fantôme échoue AUSSI) **et** du COMPTE CANONIQUE de rôles RBAC (**ancré sur CLAUDE.md §Architecture cible** « RBAC N rôles » : le nombre `50` recopié dans le contrat `rbac_50_roles.json` — le champ déclaré `cible_rbac_roles` ET la taille réelle du catalogue `len(roles)` — dans les schémas (`const: 50`) et dans la fiche `03_agents/erpnext_backend/AGENT.md` (« RBAC **50 rôles** exactement » + 3 autres mentions), sans jamais être re-dérivé du mandat ; les gates RBAC amont ne prouvent que la cohérence INTERNE contrat↔MANIFEST↔READMEs de volet, aveugles à l'ancrage au mandat — changer « RBAC 50 rôles » dans CLAUDE.md ou faire dévier le catalogue de façon interne-cohérente (cible ET len alignés ailleurs) laisserait contrat & fiche affirmer « 50 exactement » en CONTREDISANT le mandat, en silence ; recomputé de §Architecture cible : (a) contrat `cible_rbac_roles` == N, (b) taille réelle `len(roles)` == N, (c) cible == réel (mandat atteint), (d) la fiche porte l'invariant « RBAC **N rôles** exactement » + aucune mention « M rôles » périmée ; même patron que les tokens branding #4 / la persona OTOIA §Architecture cible ; un claim absent échoue AUSSI) **et** des comptes du run-book d'application `rbac/RBAC_50_ROLES_SPEC.md` §7 (la NOTE « ✅ … livré en-repo » restitue sur une surface DISTINCTE des READMEs userperm_gen/roleprofile_gen déjà gatés trois comptes data-derived — « 28 templates `Company` (portée `entite`) », « **6 profils** (5 portails métier + console technique `plateforme`) » et « **50 rôles de façon bijective** » — jamais gatés hors le §3 (ventilation par portail) ; recomputés du contrat `rbac_50_roles.json` byte-gaté : 28 == |scope==entite|, 6 == |portails distincts| (1 Role Profile par portail), 5 == portails métier hors `plateforme`, 50 == |rôles| ; bumper une portée `entite`→`groupe` ou ajouter/retirer un portail dans le contrat périme la note en silence pendant que les READMEs de volet restent verts ; le blockquote `> ` est neutralisé avant match ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ des configs runtime du Chat OTOIA dans la cellule « Métrique vérifiée » de la fiche `03_agents/frontend_console/AGENT.md` (restitue TROIS attributs data-derived du montage — `persona Amélie` · `langues FR/EN/ES` · `` `endpoint: null` `` — aveugles au bloc chat_otoia amont qui ne gate que le COMPTE blocks/mounts/portails, et NON couverts par le gate persona/capabilities ancré sur CLAUDE.md §Architecture cible (qui vise spec/mounts/MANIFEST/README/oracle du test, jamais cette fiche) ; recomputés des artefacts byte-gatés `out/chat_mount.json` (5 configs) + `out/MANIFEST.json` : persona == `MANIFEST.persona.nom` (le « QC » est le qualificatif de marque, gardé en littéral non gaté), langues `FR/EN/ES` == `MANIFEST.langues` majuscule, `endpoint: null` == **AFFIRMATION D'HONNÊTETÉ #6/#8** exigeant que CHAQUE mount ait `endpoint == null` — un endpoint fabriqué dans l'artefact fait de la fiche « null » un MENSONGE et mord ; cohérences croisées : `MANIFEST.persona.nom`/`langues` == CHAQUE mount (montage désynchronisé mordu) · `endpoint_statut == 'a_confirmer'` ⇔ endpoints TOUS null ; un claim absent échoue AUSSI) **et** des DEUX métriques data-derived du bundle SEO restituées dans la fiche `03_agents/seo/AGENT.md` (le tableau « Livrable réellement produit » recopie, à côté du compte de mots-clés déjà gaté sur la fiche, deux sorties gatées AVANT sur le seul README : L25 « graphe JSON-LD : **N nœuds** (`Organization` racine + M `Residence`) » et L26 « **N pages** × FR/EN/ES + `x-default` » ; recomputées des mêmes artefacts byte-gatés `seo_schema_org.json`/`MANIFEST.json` et `seo_hreflang.json` : L25 total nœuds == `schema_org_nodes`, racine == le `@type` singleton, M == `listings`, type listing == le `@type` uniforme des listings + cohérence arithmétique `total == 1 + M` ; L26 nombre de pages == `|pages|` de la carte + jeu de langues de contenu == celui de l'artefact (set-diff, absent ET en trop) ; RENOMMER le `@type`, AJOUTER/RETIRER un projet ou ÉCHANGER une langue périme la fiche pendant que les artefacts disent autre chose — même classe que la cellule chat_otoia de la fiche Frontend Console (fiche-agent qui restitue des attributs gatés seulement en agrégat) ; un claim absent échoue AUSSI) **et** de la colonne « Entité » de la §4 de `rbac/RBAC_50_ROLES_SPEC.md` (**ancrée sur CLAUDE.md §Entités** : la table « Cartographie rôles ↔ entités » liste en colonne de tête les 7 entités NOMMÉES du mandat `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC` + le périmètre consolidé « Groupe » — deux sources faisant autorité, CLAUDE.md §Entités pour l'orthographe & le descripteur parenthétique et `rbac_50_roles.json` pour l'ENSEMBLE des `entite_principale` réellement portées, aveugles au §3 (comptes par portail) / §7 (comptes run-book) / blocs fiches (`entite` d'UN rôle) ; (a) ENSEMBLE §4 == distinct `entite_principale` du contrat (set-diff : ni fantôme ni manquante), (b) ANCRAGE chaque entité NOMMÉE de CLAUDE.md est une ligne §4 avec son descripteur parenthétique EXACT (absence comprise), (c) chaque entité nommée du mandat est portée ≥1× par le contrat (aucune décorative) — ajouter au contrat un rôle porté par une entité absente de §4, renommer une entité dans CLAUDE.md §Entités (`9060 QC→9061 QC`) ou dériver un descripteur mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de l'ENSEMBLE des portails métier + l'exclusion nommée de la console technique dans la fiche `03_agents/frontend_console/AGENT.md` (le bloc « triplets par workspace » recompute cartes/liens/rôles PAR portail mais reste aveugle à QUELS portails composent l'ensemble ; la fiche énumère à la main L11 « 5 portails rôle métier (Ventes / Construction / Achat / Compta / Direction) » et affirme L46 que « la console technique `plateforme` (6ᵉ portail RBAC) est **exclue** » ; (a) l'ensemble ORDONNÉ des noms courts == les workspaces métier de `frontend/portails/out/MANIFEST.json` byte-gaté (label « OTO » sans préfixe, comme l'énumération des onglets Mobile) ET, DOUBLE ANCRAGE, leurs ids == `portails_business` du contrat RBAC (l'« égalité stricte » que la fiche revendique elle-même) ; (b) `plateforme` == l'UNIQUE portail RBAC hors métier (∈ portails distincts des rôles ∖ `portails_business`), c'est le 6ᵉ (total distinct == 6 = 5 métier + 1) — un échange (Achat→Marketing), un retrait, un réordre, un renommage de la console technique dans le contrat (`plateforme`→`console`) ou un contrat qui PROMEUT `plateforme` en métier mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de l'endpoint GPU RunPod ComfyUI (l'id du pod `t68akvx80wv092` + le tag « Flux dev fp8 » cités dans les fiches BIM/Rendu + le master prompt, recomputés du bullet §1 de `AGENTS_EXISTING_ASSETS.md` ; toute fiche mentionnant RunPod doit citer l'id EXACT — token backtické de forme pod == canonique — + le tag EXACT ; un pod re-créé mord les 3 fiches, la disparition totale échoue AUSSI) **et** des évènements d'émission e-CF (ENSEMBLE des évènements émetteurs « réservation/contrat » ⨯ ensemble exclu des brouillons `lead/visite/devis` **disjoint** des émetteurs — jamais de facture sur brouillon — ⨯ ENSEMBLE des `base_field` ⨯ rôle unique `compta-fiscaliste-ecf` double-ancré RBAC portail compta ⨯ FormaPago défaut `3`/Tarjeta/Cardnet #10, recomputés de `ecf_plan.json`) == 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 + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **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`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis » + **promesse anti-invention #6** `taux_pct/source/a_confirmer` tout-`null` confrontée à `commission_plan.json` dans les deux sens, invariant #7 « aucun taux sans source », `MANIFEST.counts` recoupé) **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README **ET dans les 3 copies aval : `renderer.py::_CANONICAL_MARKERS` l'oracle d'enforcement de `check_canonical`, les 2 docstrings de `scorer.py`, et l'oracle du test `test_generator.py`**, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) **et** des tokens design canoniques `publiciste/branding` (**ancrés sur CLAUDE.md #4** : les 4 valeurs de la marque luxury `#0a0a12`/`#f0b429`/`Fraunces`/`Cormorant Garamond` recopiées à QUATRE endroits jamais gatés — (a) les CONSTANTES `lib/branding.py` `COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`, hors `out/` donc invisibles à check_artifacts, importées par le générateur & substituées dans le gabarit `{{COLOR_BG}}…` ; (b) le docstring du fichier ; (c) le README ; (d) le tuple d'oracle HARDCODÉ de `tests/test_publiciste.py` que le seul test existant assert sans jamais le comparer à CLAUDE.md — recomputés de la ligne #4 de `CLAUDE.md` puis exigés EXACTS : (a) constantes == #4 (hex insensible à la casse, fontes à l'exact), (b) docstring cite les 4, (c) README cite les 2 fontes, (d) oracle du test == #4 ; même patron que les paramètres canoniques `faisabilite/generator`, un changement de #4 mord SIMULTANÉMENT les 3 copies aval ; un claim absent échoue AUSSI) **et** de l'identité du RÔLE émetteur e-CF `fiscal/ecf_dgii` (3e surface du MÊME README : le seul rôle habilité à ÉMETTRE un e-CF — bullet `role_id` README:51-54 — nommé par `role_id` « `compta-fiscaliste-ecf` » = celui que TOUS les `emission_events` d'`ecf_plan.json` byte-gaté référencent, `erpnext_role_name` « _OTO Compta Fiscaliste eCF_ », et `portail` « `compta` » résolu du contrat `rbac_50_roles.json` ; recomputés des deux sources — zéro duplication — prose EXACTE exigée ; réaffecter l'émission à un autre rôle, le renommer, ou DÉPLACER ce rôle hors du portail `compta` (e-CF émis HORS Compta, séparation cassée) mord ; cohérences croisées : `emission_events` pointent UN SEUL rôle (fonction · non vide) · `role_id` unique au contrat · nom artefact == nom contrat · `portail == compta` ; un claim absent échoue AUSSI) **et** de la colonne « Job CI » des fiches agents `03_agents/*/AGENT.md` (chaque token `*-tests` cité entre backticks — le job Gitea Actions qui exécute la suite du module — recomputé de la section `jobs:` de `.gitea/workflows/ci.yml`, jamais gaté avant : `check_ci_integrity` prouve que chaque job est CÂBLÉ dans `gate.needs` mais RIEN ne liait le NOM écrit dans la fiche au job réel ; on exige que chaque token de fiche DÉSIGNE un job existant — renommer un job dans ci.yml ou le mal recopier laisse la fiche pointer un job FANTÔME pendant que le CI reste vert ; direction fiche→ci.yml, ci.yml peut définir des jobs non cités mais une fiche ne peut citer un job absent ; la disparition de la colonne échoue AUSSI) **et** de la colonne « Entrée CLI » des fiches agents `03_agents/*/AGENT.md` (le jeu de verbes cité entre backticks — `publiciste.py parse\|validate\|generate\|run`, `faisabilite_gen.py score\|scaffold\|generate\|batch`, `rbac_apply_plan.py build\|validate`, … 16 cellules — recomputé **par AST** de l'ensemble ORDONNÉ des `subparsers.add_parser("")` du script réel, jamais gaté avant : les blocs amont couvrent les colonnes « Tests » et « Job CI » mais RIEN ne liait les VERBES écrits aux sous-commandes que le CLI EXPOSE ; script résolu par le chemin de module de la MÊME ligne de table + basename ; égalité EXACTE ensemble ET ordre exigée — renommer/ajouter/retirer/réordonner une sous-commande dans le `.py` (ou la mal recopier dans la fiche) laisse la fiche documenter une surface d'invocation FANTÔME pendant que le module tourne sous le vrai verbe ; direction fiche→CLI ; script introuvable ou disparition de la colonne échoue AUSSI) **et** de l'IDENTITÉ du DocType porteur `crm/dossier_vente` (le NOM `OTO Dossier Vente` — point d'attache du pipeline — recopié à la main dans le titre & la ligne « Nom du DocType » du README `dossier_vente`, la fiche `03_agents/crm/AGENT.md:25` et la fiche `03_agents/onapi_legal/AGENT.md:50`, jamais gaté sur le NOM (le bloc pipeline ne recompute que le COMPTE d'états) : recomputé du champ `name` de `out/doctype_oto_dossier_vente.json` byte-gaté, chaque prose exigée EXACTE + cohérences croisées `DocType.name == MANIFEST.doctype_name` & `== workflow.document_type` (l'attache porteur↔workflow — sinon le Workflow vise un DocType FANTÔME) ; renommer le DocType dans le spec reconstruit les 2 artefacts cohéremment mais périme la prose en silence ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ du nœud racine `Organization` du graphe schema.org SEO (**ancrée sur CLAUDE.md §Entités** : le nom de marque `Helios RD` (marque publique) + son `url` == base_url — le bloc composition amont ne gate que les `@type`/bijection, jamais l'IDENTITÉ de marque que Google indexe sous `vente.otov7.com` ; recomputés de `seo_schema_org.json` byte-gaté + `MANIFEST.base_url` + le `country_code` du spec : (a) `Organization.name` == marque re-dérivée de CLAUDE.md §Entités — renommer la marque dans le spec ou dans CLAUDE.md émet un JSON-LD public qui CONTREDIT le mandat tout en restant byte-vert (l'artefact se reconstruit fidèlement depuis un spec dérivé), même « vert trompeur » que les tokens branding ancrés sur #4 — (b) `Organization.url` == base_url & `@id` sous base_url, (c) chaque listing rattaché à CETTE Organization (`brand.@id`) sous base_url, (d) `addressCountry` UNIFORME == `country_code` du spec, (e) le spec DÉCLARE l'ancrage (`organization.source` cite CLAUDE.md)) **et** des entités porteuses du DocType CONFOTUR `legal/confotur` (**ancrées sur CLAUDE.md §Entités** : le champ Select `entite_porteuse` offre les 7 entités canoniques `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC`, dérivées via `options_source` de `confotur_spec.json[entites]` et byte-gatées pour la reproductibilité mais jamais ancrées au mandat — les blocs CONFOTUR amont ne gatent que le COMPTE de champs/rôles et la cross-cohérence des permissions ; recomputées des tokens en gras de la section « ## Entités » de CLAUDE.md : (a) options de l'artefact == entités CLAUDE.md ORDRE exact, (b) `spec[entites]` == CLAUDE.md l'INPUT ancré, (c) le spec DÉCLARE l'ancrage `entites_source` cite CLAUDE.md + chaque entité, (d) wiring intact `options_source == "entites"`, (e) README cite l'ancre CLAUDE.md + nomme chaque entité par nom complet OU 1er token — renommer une entité dans CLAUDE.md seul mord SIMULTANÉMENT artefact/spec/déclaration/README, l'ancre est vive ; un claim absent échoue AUSSI) **et** de la cross-cohérence PERMISSIONS CONFOTUR répliquée dans la FICHE AGENT `03_agents/onapi_legal/AGENT.md` (3e surface : la §Permissions de la fiche recopie la MÊME table role→portail→actions que le README du module — `ventes-confotur` (Ventes)→read/write/create/print · `legal-onapi` (Direction)→read/write/create · `legal-directeur` (Direction)→read/write/**submit**/report — sous une convention DIFFÉRENTE (` / ` espacé · gras `**submit**`), gatée par RIEN alors que le gate README ne voit QUE le README ; recomputées de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`, byte-gaté), gras/casse normalisés, set-diff par rôle + identité d'ensemble fiche⇔MANIFEST — promouvoir un rôle à `submit`, retirer une action, réaffecter un portail, renommer/ajouter un rôle fantôme dans la fiche mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de la persona & des capabilities OTOIA du Chat par portail `frontend/chat_otoia` (**ancrées sur CLAUDE.md §Architecture cible** : la persona `Amélie` / voix `multilingual_v2` + les 4 capabilities `aec.py`+`knowledge.py`+`prompt_engine.py`+`chat.py`, portées par `out/chat_mount.json` (5 mounts) & `out/MANIFEST.json` byte-gatés pour la reproductibilité mais jamais ancrées au mandat — le bloc chat_otoia amont ne gate que la COMPOSITION (portails métier · 1 block⇔1 mount) et le SEUL contrôle d'identité `tests/` a un ORACLE HARDCODÉ jamais comparé à CLAUDE.md ; recomputées de CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) » · « OTOIA capabilities : … ») : (a) `chat_spec.json` persona/capabilities == CLAUDE.md, (b) le spec DÉCLARE l'ancrage (`persona.source` + chaque `capability.source` citent CLAUDE.md), (c) CHACUN des 5 mounts + le MANIFEST portent persona/capabilities == CLAUDE.md, (d) le README cite l'ancre §Architecture cible + nomme persona + chaque capability, (e) l'ORACLE du test == CLAUDE.md — renommer la persona/voix ou renommer/retirer une capability dans le spec (ou dans CLAUDE.md) mord tout en restant byte-vert ET tests-verts, même « vert trompeur » que les tokens branding ancrés sur #4 ; un claim absent échoue AUSSI) **et** du SDK MAJEUR Expo de l'app mobile `mobile/app_config` (**ancré sur la roadmap Sprint 5 l.56** « Rebuild Expo 54 » : la version majeure `expoSdkMajor` que l'app cible — recopiée CINQ fois dans le README (`Expo 54`/`Expo SDK **54**`) — jamais gatée hors le quadruplet onglets/rôles/langues/identifiants ; recomputée du champ `expo.extra.expoSdkMajor` de `out/app_config.json` byte-gaté : (a) cohérence interne artefact ⇔ `MANIFEST.app.expo_sdk_major` ⇔ `mobile_spec.json[app].expo_sdk_major`, (b) ANCRAGE roadmap `Rebuild Expo ` == artefact (ancre vive : bumper le SDK dans l'artefact ou dans la roadmap sans l'autre mord), (c) le spec DÉCLARE l'ancrage (`expo_sdk_source` cite roadmap + Expo N), (d) TOUTE mention prose « Expo [SDK] N » == artefact (aucune périmée), (e) la ligne de source du tableau cite l'ancre `roadmap` ; un claim absent échoue AUSSI) **et** du jeu de DEVISES `moneda` de l'e-CF `fiscal/ecf_dgii` (**ancré sur CLAUDE.md #10** : les options `USD`/`DOP` du champ `TipoMoneda`, byte-gatées dans `out/ecf_plan.json` mais jamais ancrées au mandat — les blocs fiscaux amont gatent le FormaPago (Cardnet) et les états/champs émetteurs, aveugles au jeu de devises, et le bloc « Fiches #10 » ancre les fiches ERPNext/CRM, aveugle au module fiscal ; recomputées de la devise en gras de la contrainte #10 (`USD + DOP`, réutilise l'`exp["devises"]` déjà parsé · zéro duplication) : (a) `spec[moneda].options` == #10 ORDRE exact (mord ajout/retrait/réordonnancement), (b) l'artefact byte-gaté idem, (c) le spec DÉCLARE l'ancrage via un champ DISTINCT `options_source` — `source` restant legit. `null` (taux TipoCambio à confirmer · #8) — qui cite `CLAUDE.md #10` + chaque devise, (d) la prose README qui se réclame de #10 cite le jeu EXACT + toute mention `moneda`/`devise` d'une paire de devises concordante — étendre/renommer une devise dans #10 ET le spec de façon cohérente reste byte-vert tout en faisant émettre un `TipoMoneda` hors mandat, même « vert trompeur » que la marque SEO §Entités ou les tokens branding #4 ; un claim absent échoue AUSSI) **et** de l'IMPUTABILITÉ des confirmations préalables VPS `devops/deploy_runbook` (3e surface : la ligne « Confirmations préalables VPS » du README annote chaque confirmation d'un couple `nom` (**owner** · **réf-contrôle**) — ex. `taux_commission` (Direction · audit_5d D1.1) — data-derived des `confirmations` de `out/deploy_runbook.json` byte-gaté CROISÉES avec le rapport `qa/audit_5d` byte-gaté ; les blocs DevOps amont ne gatent que le COMPTE et l'ENSEMBLE des noms, aveugles à QUI porte chaque confirmation et à QUEL contrôle la trace ; cross-anchor à trois : owner run-book ⊆ owner du contrôle audit_5d cité (tolère le suffixe « / UAF ») · owner README ⊆ owner run-book (tolère l'abréviation « Fiscaliste eCF » ⊂ « Compta Fiscaliste eCF ») · réf README == contrôle réel présent dans audit_5d ET dans la source — réaffecter un owner, misattribuer le contrôle (`D1.1`→`D2.1`) ou citer un contrôle FANTÔME (`D9.9`) mord ; une confirmation tracée DOIT rester annotée, une annotation d'un nom fantôme échoue AUSSI) **et** du COMPTE CANONIQUE de rôles RBAC (**ancré sur CLAUDE.md §Architecture cible** « RBAC N rôles » : le nombre `50` recopié dans le contrat `rbac_50_roles.json` — le champ déclaré `cible_rbac_roles` ET la taille réelle du catalogue `len(roles)` — dans les schémas (`const: 50`) et dans la fiche `03_agents/erpnext_backend/AGENT.md` (« RBAC **50 rôles** exactement » + 3 autres mentions), sans jamais être re-dérivé du mandat ; les gates RBAC amont ne prouvent que la cohérence INTERNE contrat↔MANIFEST↔READMEs de volet, aveugles à l'ancrage au mandat — changer « RBAC 50 rôles » dans CLAUDE.md ou faire dévier le catalogue de façon interne-cohérente (cible ET len alignés ailleurs) laisserait contrat & fiche affirmer « 50 exactement » en CONTREDISANT le mandat, en silence ; recomputé de §Architecture cible : (a) contrat `cible_rbac_roles` == N, (b) taille réelle `len(roles)` == N, (c) cible == réel (mandat atteint), (d) la fiche porte l'invariant « RBAC **N rôles** exactement » + aucune mention « M rôles » périmée ; même patron que les tokens branding #4 / la persona OTOIA §Architecture cible ; un claim absent échoue AUSSI) **et** des comptes du run-book d'application `rbac/RBAC_50_ROLES_SPEC.md` §7 (la NOTE « ✅ … livré en-repo » restitue sur une surface DISTINCTE des READMEs userperm_gen/roleprofile_gen déjà gatés trois comptes data-derived — « 28 templates `Company` (portée `entite`) », « **6 profils** (5 portails métier + console technique `plateforme`) » et « **50 rôles de façon bijective** » — jamais gatés hors le §3 (ventilation par portail) ; recomputés du contrat `rbac_50_roles.json` byte-gaté : 28 == |scope==entite|, 6 == |portails distincts| (1 Role Profile par portail), 5 == portails métier hors `plateforme`, 50 == |rôles| ; bumper une portée `entite`→`groupe` ou ajouter/retirer un portail dans le contrat périme la note en silence pendant que les READMEs de volet restent verts ; le blockquote `> ` est neutralisé avant match ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ des configs runtime du Chat OTOIA dans la cellule « Métrique vérifiée » de la fiche `03_agents/frontend_console/AGENT.md` (restitue TROIS attributs data-derived du montage — `persona Amélie` · `langues FR/EN/ES` · `` `endpoint: null` `` — aveugles au bloc chat_otoia amont qui ne gate que le COMPTE blocks/mounts/portails, et NON couverts par le gate persona/capabilities ancré sur CLAUDE.md §Architecture cible (qui vise spec/mounts/MANIFEST/README/oracle du test, jamais cette fiche) ; recomputés des artefacts byte-gatés `out/chat_mount.json` (5 configs) + `out/MANIFEST.json` : persona == `MANIFEST.persona.nom` (le « QC » est le qualificatif de marque, gardé en littéral non gaté), langues `FR/EN/ES` == `MANIFEST.langues` majuscule, `endpoint: null` == **AFFIRMATION D'HONNÊTETÉ #6/#8** exigeant que CHAQUE mount ait `endpoint == null` — un endpoint fabriqué dans l'artefact fait de la fiche « null » un MENSONGE et mord ; cohérences croisées : `MANIFEST.persona.nom`/`langues` == CHAQUE mount (montage désynchronisé mordu) · `endpoint_statut == 'a_confirmer'` ⇔ endpoints TOUS null ; un claim absent échoue AUSSI) **et** des DEUX métriques data-derived du bundle SEO restituées dans la fiche `03_agents/seo/AGENT.md` (le tableau « Livrable réellement produit » recopie, à côté du compte de mots-clés déjà gaté sur la fiche, deux sorties gatées AVANT sur le seul README : L25 « graphe JSON-LD : **N nœuds** (`Organization` racine + M `Residence`) » et L26 « **N pages** × FR/EN/ES + `x-default` » ; recomputées des mêmes artefacts byte-gatés `seo_schema_org.json`/`MANIFEST.json` et `seo_hreflang.json` : L25 total nœuds == `schema_org_nodes`, racine == le `@type` singleton, M == `listings`, type listing == le `@type` uniforme des listings + cohérence arithmétique `total == 1 + M` ; L26 nombre de pages == `|pages|` de la carte + jeu de langues de contenu == celui de l'artefact (set-diff, absent ET en trop) ; RENOMMER le `@type`, AJOUTER/RETIRER un projet ou ÉCHANGER une langue périme la fiche pendant que les artefacts disent autre chose — même classe que la cellule chat_otoia de la fiche Frontend Console (fiche-agent qui restitue des attributs gatés seulement en agrégat) ; un claim absent échoue AUSSI) **et** de la colonne « Entité » de la §4 de `rbac/RBAC_50_ROLES_SPEC.md` (**ancrée sur CLAUDE.md §Entités** : la table « Cartographie rôles ↔ entités » liste en colonne de tête les 7 entités NOMMÉES du mandat `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC` + le périmètre consolidé « Groupe » — deux sources faisant autorité, CLAUDE.md §Entités pour l'orthographe & le descripteur parenthétique et `rbac_50_roles.json` pour l'ENSEMBLE des `entite_principale` réellement portées, aveugles au §3 (comptes par portail) / §7 (comptes run-book) / blocs fiches (`entite` d'UN rôle) ; (a) ENSEMBLE §4 == distinct `entite_principale` du contrat (set-diff : ni fantôme ni manquante), (b) ANCRAGE chaque entité NOMMÉE de CLAUDE.md est une ligne §4 avec son descripteur parenthétique EXACT (absence comprise), (c) chaque entité nommée du mandat est portée ≥1× par le contrat (aucune décorative) — ajouter au contrat un rôle porté par une entité absente de §4, renommer une entité dans CLAUDE.md §Entités (`9060 QC→9061 QC`) ou dériver un descripteur mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de l'ENSEMBLE des portails métier + l'exclusion nommée de la console technique dans la fiche `03_agents/frontend_console/AGENT.md` (le bloc « triplets par workspace » recompute cartes/liens/rôles PAR portail mais reste aveugle à QUELS portails composent l'ensemble ; la fiche énumère à la main L11 « 5 portails rôle métier (Ventes / Construction / Achat / Compta / Direction) » et affirme L46 que « la console technique `plateforme` (6ᵉ portail RBAC) est **exclue** » ; (a) l'ensemble ORDONNÉ des noms courts == les workspaces métier de `frontend/portails/out/MANIFEST.json` byte-gaté (label « OTO » sans préfixe, comme l'énumération des onglets Mobile) ET, DOUBLE ANCRAGE, leurs ids == `portails_business` du contrat RBAC (l'« égalité stricte » que la fiche revendique elle-même) ; (b) `plateforme` == l'UNIQUE portail RBAC hors métier (∈ portails distincts des rôles ∖ `portails_business`), c'est le 6ᵉ (total distinct == 6 = 5 métier + 1) — un échange (Achat→Marketing), un retrait, un réordre, un renommage de la console technique dans le contrat (`plateforme`→`console`) ou un contrat qui PROMEUT `plateforme` en métier mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de l'endpoint GPU RunPod ComfyUI (l'id du pod `t68akvx80wv092` + le tag « Flux dev fp8 » cités dans les fiches BIM/Rendu + le master prompt, recomputés du bullet §1 de `AGENTS_EXISTING_ASSETS.md` ; toute fiche mentionnant RunPod doit citer l'id EXACT — token backtické de forme pod == canonique — + le tag EXACT ; un pod re-créé mord les 3 fiches, la disparition totale échoue AUSSI) **et** des évènements d'émission e-CF (ENSEMBLE des évènements émetteurs « réservation/contrat » ⨯ ensemble exclu des brouillons `lead/visite/devis` **disjoint** des émetteurs — jamais de facture sur brouillon — ⨯ ENSEMBLE des `base_field` ⨯ rôle unique `compta-fiscaliste-ecf` double-ancré RBAC portail compta ⨯ FormaPago défaut `3`/Tarjeta/Cardnet #10, recomputés de `ecf_plan.json`) **et** de l'identité d'infrastructure VPS (IP `153.75.250.214` + les DEUX conteneurs Docker `erpnext-backend-1`/`erpnext-frontend-1` recomputés de CLAUDE.md §VPS ; direction copie→source — toute doc `.md` du mandat, hors journaux `05_activity_log`/`daily_reports`, citant une IPv4 ou un conteneur de forme `--1` doit == la valeur canonique : une migration serveur ou un renommage du projet compose mord toutes les copies périmées, disparition totale de la surface échoue AUSSI) == 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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index db42f49..d62a9f8 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -5802,6 +5802,116 @@ if _ecf_ev is not None: good(f"Fiscal e-CF évènements · FormaPago défaut = {_fp_code} " f"({_lbl}) · Cardnet #10 == ecf_plan") +# ============================================================================ +# CLAUDE.md §VPS · IDENTITÉ des IDENTIFIANTS D'INFRASTRUCTURE (IP + conteneurs +# Docker) — même classe de « vert trompeur » que l'endpoint RunPod, côté serveur. +# ---------------------------------------------------------------------------- +# CLAUDE.md §VPS FIXE l'infrastructure de production du mandat : +# • l'IP du VPS `153.75.250.214` +# • les DEUX conteneurs Docker `erpnext-backend-1` / `erpnext-frontend-1` +# Ces identifiants sont RECOPIÉS À LA MAIN dans quantité de docs — README.md, +# le master prompt, AGENTS_EXISTING_ASSETS.md, la fiche `mobile` (backend cible de +# l'API), RBAC_50_ROLES_SPEC.md (run-book d'import), tests/README.md (base URL +# Playwright), ci/README.md (instance Gitea) — sans AUCUN ancrage vers CLAUDE.md. +# Aucun gate ne couvrait ce lien : check_docs ne voit qu'un lien Markdown, +# guard_constraints qu'un terme interdit, check_artifacts qu'un out/ — une IP ni un +# nom de conteneur n'est aucun des trois. Or une MIGRATION serveur (nouvelle IP) ou +# un RENOMMAGE du projet compose (`erpnext_*` → `oto_*`) est un évènement ops +# COURANT : CLAUDE.md §VPS se met à jour, les ~10 copies se PÉRIMENT en silence → +# un agent câblerait l'API mobile / les tests e2e / le hand-off d'import vers un +# serveur MORT ou un conteneur FANTÔME (l'invention même que #6 interdit). Aucune +# suite tests/ (FONCTIONS des modules, jamais la prose infra) ne l'attrape — classe +# notée en mémoire (`agent-fiche-role-attrs-ungated`, surface infra ~ RunPod). +# +# On RE-DÉRIVE l'IP + l'ENSEMBLE des conteneurs de CLAUDE.md §VPS (jamais une liste +# à la main · #6). Direction copie→source : sur TOUTE doc .md du mandat (hors +# CLAUDE.md = la source, hors 05_activity_log/ + daily_reports/ = journaux +# historiques figés, à ne pas réécrire), tout token IPv4 DOIT == l'IP canonique et +# tout token conteneur `erpnext--1` DOIT ∈ l'ensemble canonique. Le corpus ne +# porte QUE ces valeurs (aucune autre IPv4, aucun autre conteneur — vérifié) : un +# écart = une copie périmée. Le write-path interdit `/var/www/html/static` relève +# de guard_constraints (#interdits), non de ce gate d'identité. Garde +# anti-évaporation : l'IP ET ≥1 conteneur canonique doivent rester cités quelque +# part (sinon la surface d'identité du VPS a silencieusement disparu · #6). +# ============================================================================ +VPS_SRC = "CLAUDE.md" +try: + _vps_cm = open(VPS_SRC, encoding="utf-8").read() +except OSError as e: + bad(f"VPS infra · {VPS_SRC} illisible : {e}"); _vps_cm = None + +if _vps_cm is not None: + _vps_sec = re.search(r"^## VPS\s*\n(.*?)(?=^## |\Z)", _vps_cm, re.S | re.M) + if _vps_sec is None: + bad(f"VPS infra · section « ## VPS » INTROUVABLE dans {VPS_SRC} → ancre " + f"d'infra cassée, impossible de recomputer IP/conteneurs") + else: + _vps_body = _vps_sec.group(1) + _ip_m = re.search(r"\bIP\s+(\d{1,3}(?:\.\d{1,3}){3})\b", _vps_body) + _dk_m = re.search(r"Docker\s*\(([^)]+)\)", _vps_body) + if _ip_m is None or _dk_m is None: + bad(f"VPS infra · §VPS ne livre pas l'IP et/ou les conteneurs Docker " + f"(IP={_ip_m and _ip_m.group(1)} · Docker={_dk_m and _dk_m.group(1)}) " + f"→ format de §VPS modifié, recompute impossible") + else: + vps_ip = _ip_m.group(1) + vps_containers = {c.strip() for c in _dk_m.group(1).split(",") + if re.fullmatch(r"[a-z0-9]+-[a-z]+-1", c.strip())} + # Enveloppe de forme DÉRIVÉE des stems de service canoniques + # (backend/frontend), PAS du préfixe compose littéral `erpnext-` : un + # RENOMMAGE du projet compose (`erpnext-backend-1` → `oto-backend-1`) + # dans une copie DOIT être vu comme fantôme — l'ancrer sur `erpnext-` + # le rendrait invisible (le token renommé ne matcherait plus). + _stems = sorted({m.group(1) for c in vps_containers + if (m := re.fullmatch(r"[a-z0-9]+-([a-z]+)-1", c))}) + if not vps_containers or not _stems: + bad(f"VPS infra · §VPS « Docker (…) » ne contient aucun conteneur de " + f"forme `--1` (lu {_dk_m.group(1)!r}) → format modifié") + else: + _cont_shape = re.compile(r"[a-z0-9]+-(?:" + "|".join(_stems) + r")-1") + _ip_shape = re.compile(r"\b\d{1,3}(?:\.\d{1,3}){3}\b") + vps_files = [f for f in subprocess.run( + ["git", "ls-files", "*.md"], + capture_output=True, text=True).stdout.split() + if f != "CLAUDE.md" + and not f.startswith("05_activity_log/") + and not f.startswith(f"{D}/daily_reports/")] + ip_seen = cont_seen = 0 + for vf in vps_files: + try: + vtxt = open(vf, encoding="utf-8").read() + except OSError as e: + bad(f"VPS infra · {vf} illisible : {e}"); continue + # (a) IPv4 : toute IP citée == IP canonique du §VPS + ips = set(_ip_shape.findall(vtxt)) + if ips: + ip_seen += 1 + stale_ip = sorted(ips - {vps_ip}) + if stale_ip: + bad(f"VPS infra · {vf} cite l'IP périmée {stale_ip} MAIS " + f"CLAUDE.md §VPS dit `{vps_ip}` → serveur migré / IP " + f"mal recopiée (mettre la doc à jour)") + else: + good(f"VPS infra · {vf} · IP `{vps_ip}` == §VPS") + # (b) conteneurs Docker : tout `erpnext--1` ∈ ensemble canonique + conts = set(_cont_shape.findall(vtxt)) + if conts: + cont_seen += 1 + phantom = sorted(conts - vps_containers) + if phantom: + bad(f"VPS infra · {vf} cite le(s) conteneur(s) fantôme(s) " + f"{phantom} MAIS §VPS dit {sorted(vps_containers)} → " + f"projet compose renommé / nom mal recopié") + else: + good(f"VPS infra · {vf} · conteneur(s) {sorted(conts)} " + f"⊆ §VPS {sorted(vps_containers)}") + if ip_seen == 0: + bad(f"VPS infra · AUCUNE doc du mandat ne cite l'IP `{vps_ip}` — la " + f"surface d'identité du VPS s'est évaporée (régression #6)") + if cont_seen == 0: + bad(f"VPS infra · AUCUNE doc ne cite de conteneur Docker " + f"`erpnext--1` — surface d'identité conteneur évaporée (#6)") + sys.exit(1 if FAIL else 0) PY rc=$?