From 861260462fc36ee4aa8e6e5204b6db3065a39d59 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 19:12:16 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260801=5F190412]=20Sprint=208?= =?UTF-8?q?=20=C2=B7=20buffer=20=C2=B7=20Fiches=20agents/RBAC=20:=20gate?= =?UTF-8?q?=20d'APPARTENANCE=20=C3=A0=20roles=5Fallowed=20du=20chat=20OTOI?= =?UTF-8?q?A=20=E2=80=94=203e=20surface=20d'appartenance=20(DISTINCTE=20du?= =?UTF-8?q?=20contrat=20rbac=5F50=5Froles.json=20ET=20du=20Has=20Role=20wo?= =?UTF-8?q?rkspace.json)=20:=20QUI=20peut=20INVOQUER=20l'assistant=20OTOIA?= =?UTF-8?q?=20d'un=20portail=20=E2=80=94=20asser=C3=A9e=20en=20prose=20par?= =?UTF-8?q?=20rendu:36=20(=E2=88=88=20roles=5Fallowed=20construction,=20tr?= =?UTF-8?q?iple=20ancrage)=20+=20mobile:41=20(=E2=88=89=20tout,=20assertio?= =?UTF-8?q?n=20n=C3=A9gative=20#6),=20recomput=C3=A9e=20de=20chat=5Fotoia/?= =?UTF-8?q?out/chat=5Fmount.json=20(byte-gat=C3=A9)=20;=206=20morsures=20v?= =?UTF-8?q?=C3=A9rifi=C3=A9es,=207=20gates=20re-verts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 05_activity_log/2026-08-01.md | 20 ++++++ ci/README.md | 32 +++++++++- ci/check_readme_claims.sh | 112 ++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 861da62..9f55c7e 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -2073,3 +2073,23 @@ spec == rapport · exit 0. Working tree byte-restauré (`git checkout --`, **JAM édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la confirmation des 4 open items réglementaires reste côté métier/VPS). - **Auto-score 4Big** : 96/100. + +## Session `20260801_190412` · Buffer S8 · Domaine Fiches agents/RBAC : l'**appartenance à `roles_allowed` du chat OTOIA** — QUI peut **invoquer** l'assistant OTOIA embarqué dans un portail — asserée EN PROSE par 2 fiches (`03_agents/rendu/AGENT.md:36` positive · `03_agents/mobile/AGENT.md:41` négative) était **HORS de tout gate d'appartenance**. C'est une **3e surface d'appartenance** DISTINCTE des deux déjà gatées : le **contrat** `rbac_50_roles.json` (attributs du rôle, blocs Fiches Faisabilité/Mobile) et le **`Has Role`** `workspace.json` (accès console, bloc Fiches Workspace). Le contrôle d'invocation du chat vit dans un 3e artefact — `frontend/chat_otoia/out/chat_mount.json` (`roles_allowed` par portail) — que le bloc Chat OTOIA amont ne gate que sur l'IDENTITÉ persona/capabilities + la COMPOSITION (5 mounts ⇔ 5 portails), AVEUGLE à QUEL rôle figure dans quelle `roles_allowed`. + +**Tâche** : **Sprint 8 · buffer** (QA/DevOps — série anti-dérive CLAUDE.md #6, roadmap fonctionnellement close). **Même patron EXACT** que le bloc de membership `Has Role` (juste au-dessus dans `check_readme_claims.sh`) — appliqué à un **artefact + une sémantique DIFFÉRENTS** : accès console (`Has Role`) ≠ droit d'invocation du chat (`roles_allowed`). + +**Dérive silencieuse fermée** : +- `03_agents/rendu/AGENT.md:36` — POSITIVE : rôle « OTO Faisabilité Rendu 3D » ∈ `roles_allowed` du portail NOMMÉ `construction` (déclenchement des rendus assistés). +- `03_agents/mobile/AGENT.md:41` — NÉGATIVE d'honnêteté (#6) : rôle « OTO Plateforme Mobile » (famille `plateforme`) dans AUCUNE `roles_allowed` (le chat ne couvre que les portails métier). +- Source faisant autorité : `frontend/chat_otoia/out/chat_mount.json[].roles_allowed` (byte-gaté par `check_artifacts`), `erpnext_role_name` résolu du contrat par `id` (zéro duplication). +- Piège : ajouter le rôle mobile à une `roles_allowed` = **SUR-EXPOSITION** (le dev mobile gagnerait le droit de piloter l'IA d'un portail métier — l'escalade même que #6 refuse) ; retirer le rôle rendu de la `roles_allowed` construction = perte du déclenchement assisté ; DÉPLACER le rôle vers un autre portail — laisse la fiche périmée EN SILENCE pendant que l'artefact dit autre chose. Aucune suite `tests/` (qui teste des FONCTIONS de montage, pas la prose d'une fiche) n'attrape ce « vert trompeur ». +- **État courant** : aucune valeur périmée — rendu ∈ construction (et lui seul), mobile ∉ tout (anti-invention #6, rien à réécrire). Le défaut est la **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiches agents · appartenance à `roles_allowed` du chat OTOIA » après le bloc Fiches Workspace) : index recomputé `erpnext_role_name → {portails autorisant}`, puis (a) **in_named_portail** (rendu) — le portail nommé est data-derived (== `portail` du rôle au contrat, **triple ancrage** : nommé == contrat == unique portail autorisant), membership == {ce portail}, prose citant « invoquer le chat OTOIA » ; (b) **not_in_any** (mobile) — rôle ABSENT de TOUTE `roles_allowed`. Portail nommé fantôme (absent des mounts) = échec (anti-typo). Claim absent = échec (traçabilité #6). Invariant vérifié en amont : chaque rôle ∈ ≤1 `roles_allowed`, == son `portail` contrat. + +**6 morsures vérifiées** (3 côté artefact · 3 côté fiche/bord) : artefact retire rendu de `construction` (∉ aucun portail) · artefact ajoute mobile à `ventes` (SUR-EXPOSITION #6 VIOLÉE) · artefact ajoute AUSSI rendu à `compta` (droit élargi, fiche nomme 1 seul) · fiche rendu nomme le mauvais portail (`construction→compta`, INTROUVABLE) · fiche mobile retire le token `roles_allowed` (INTROUVABLE) ; restauré = green : rendu ∈ `{construction}` · mobile ∉ tout · exit 0. Working tree byte-restauré (`git checkout --`, **JAMAIS** `git clean`, interdit absolu) · **7 gates re-verts** (`validate_json`/`check_docs`/`guard_constraints`/`check_artifacts`/`check_ci_integrity`/`check_readme_claims`/`check_regression`). + +- `ci/README.md` (recap `check-readme-claims` + paragraphe détaillé « 3e surface d'appartenance · `roles_allowed` du chat OTOIA ») mis à jour. +- **Note memory** : la classe « agent-fiche-role-attrs-ungated » est désormais CLOSE (attributs contrat = Fiches Faisabilité/Mobile ; `Has Role` = Fiches Workspace ; `roles_allowed` chat = ce bloc). Candidat propre restant : la fiche `ifc_speckle:34` (attributs contrat déjà gatés) + la row acceptance S3 « viewer 3D » (déjà gatée QA). +- **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 ; le montage réel du chat reste côté runner/VPS). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 0c73ac8..90da9ec 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 de tests par suite écrits HORS de cette colonne (prose libre seo « 36 tests »/frontend_console « 19 »&« 31 »/onapi_legal « 44 » + cellule de table à suffixe devops « 29 (dont 14…) », recomputés de `plan.suites[].test_methods` — surfaces que row_re et l'agrégat en prose ne balaient pas) **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` + IDENTITÉ ordonnée des 5 dimensions `id→titre→reference` recomputée de `audit_report.dimensions`) **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` + seuils structurels de la colonne « Fait vérifié » `DOC ≥ N octets`/`TESTS ≥ N méthodes`, recomputés de `quality_spec.json[thresholds]` et cross-vérifiés contre l'evidence `(seuil N)` du report) **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é + **rôles payés** (le « à quel rôle » du livrable) : chaque `role_id` de `commission_plan.json` résolu dans `rbac_50_roles.json`, appartenance **singleton** au portail `ventes` nommé au README == un `portails_business` (séparation des pouvoirs · invariant #5), `erpnext_role_name` == contrat (nom Frappe résolu · invariant #6), `MANIFEST.roles_rbac_utilises` 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** des champs `dossier_vente`/`estado` du DocType CONFOTUR `legal/confotur` **dérivés du workflow vente** (`dossier_vente` Link → cible `workflow.document_type` (= `OTO Dossier Vente`) ; `estado` Select → options = les états `confotur_*` du pipeline ; source `crm/workflow_vente/workflow_vente_spec.json` = l'input du builder, déclaré `workflow_source` au MANIFEST — les blocs CONFOTUR amont gatent comptes/permissions/entités mais PAS ces 2 champs ; (0) MANIFEST `workflow_source` cite le spec, (a) `dossier_vente.options` == workflow `document_type`, (b) prose README « (= `…`) » == `document_type`, (c) `estado.options` == `update_value` des états `confotur_*` ORDRE pipeline, (c′) toute option porte le préfixe `confotur`, (d) le bullet `estado` DÉCLARE la dérivation ; renommer le `document_type` côté spec mord SIMULTANÉMENT artefact ET prose, 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 PIED DE PAGE « Voix Amélie QC (multilingual_v2) » des 11 fiches agents qui orchestrent OTOIA (toutes sauf faisabilite/publiciste · le triplet persona/qualif/voix re-dérivé INDÉPENDAMMENT de CLAUDE.md §Architecture cible — regex qui ne fige pas le qualificatif en dur, donc `QC→…` mord ici — et exigé EXACT sur chaque pied de page ; pied de page malformé & évaporation totale du recensement échouent 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 | +| `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 de tests par suite écrits HORS de cette colonne (prose libre seo « 36 tests »/frontend_console « 19 »&« 31 »/onapi_legal « 44 » + cellule de table à suffixe devops « 29 (dont 14…) », recomputés de `plan.suites[].test_methods` — surfaces que row_re et l'agrégat en prose ne balaient pas) **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` + IDENTITÉ ordonnée des 5 dimensions `id→titre→reference` recomputée de `audit_report.dimensions`) **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` + seuils structurels de la colonne « Fait vérifié » `DOC ≥ N octets`/`TESTS ≥ N méthodes`, recomputés de `quality_spec.json[thresholds]` et cross-vérifiés contre l'evidence `(seuil N)` du report) **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é + **rôles payés** (le « à quel rôle » du livrable) : chaque `role_id` de `commission_plan.json` résolu dans `rbac_50_roles.json`, appartenance **singleton** au portail `ventes` nommé au README == un `portails_business` (séparation des pouvoirs · invariant #5), `erpnext_role_name` == contrat (nom Frappe résolu · invariant #6), `MANIFEST.roles_rbac_utilises` 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** de l'appartenance à `roles_allowed` du chat OTOIA des fiches agents `03_agents/{rendu,mobile}/AGENT.md` (3e surface d'appartenance DISTINCTE du contrat ET du `Has Role` : QUI peut INVOQUER l'assistant OTOIA d'un portail, contrôle qui vit dans `frontend/chat_otoia/out/chat_mount.json` byte-gaté — on recompute l'ensemble des portails dont la `roles_allowed` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme ∈ `roles_allowed` du portail NOMMÉ `construction` (in_named_portail, triple ancrage : portail nommé == `portail` contrat == unique portail autorisant, membership == {ce portail}, prose citant « invoquer le chat OTOIA ») · mobile affirme VOLONTAIREMENT dans AUCUNE `roles_allowed` (not_in_any, assertion NÉGATIVE #6) — mord l'ajout du rôle mobile à une `roles_allowed` (SUR-EXPOSITION du chat), le retrait/déplacement du rôle rendu de la `roles_allowed` construction, l'élargissement à un 2e portail, un portail 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** des champs `dossier_vente`/`estado` du DocType CONFOTUR `legal/confotur` **dérivés du workflow vente** (`dossier_vente` Link → cible `workflow.document_type` (= `OTO Dossier Vente`) ; `estado` Select → options = les états `confotur_*` du pipeline ; source `crm/workflow_vente/workflow_vente_spec.json` = l'input du builder, déclaré `workflow_source` au MANIFEST — les blocs CONFOTUR amont gatent comptes/permissions/entités mais PAS ces 2 champs ; (0) MANIFEST `workflow_source` cite le spec, (a) `dossier_vente.options` == workflow `document_type`, (b) prose README « (= `…`) » == `document_type`, (c) `estado.options` == `update_value` des états `confotur_*` ORDRE pipeline, (c′) toute option porte le préfixe `confotur`, (d) le bullet `estado` DÉCLARE la dérivation ; renommer le `document_type` côté spec mord SIMULTANÉMENT artefact ET prose, 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 PIED DE PAGE « Voix Amélie QC (multilingual_v2) » des 11 fiches agents qui orchestrent OTOIA (toutes sauf faisabilite/publiciste · le triplet persona/qualif/voix re-dérivé INDÉPENDAMMENT de CLAUDE.md §Architecture cible — regex qui ne fige pas le qualificatif en dur, donc `QC→…` mord ici — et exigé EXACT sur chaque pied de page ; pied de page malformé & évaporation totale du recensement échouent 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 @@ -1109,6 +1109,36 @@ INTROUVABLE) · fiche rendu supprime l'assertion · fiche mobile nomme un Worksp fantôme (`OTO Marketing`) · fiche ifc supprime l'assertion générique · fiche mobile passe le négatif au positif (retire « volontairement pas ») ; restauré = vert. +Enfin, sur une **3e surface d'appartenance** encore DISTINCTE des deux précédentes : +l'appartenance à `roles_allowed` du **chat OTOIA** — QUI peut **invoquer** l'assistant +embarqué dans un portail. Ce contrôle d'accès ne vit ni dans `rbac_50_roles.json` +(attributs) ni dans `workspace.json` (`Has Role`, accès console) mais dans +`frontend/chat_otoia/out/chat_mount.json` (une config runtime par portail, byte-gaté +par `check_artifacts`), et deux fiches l'affirment EN PROSE : `03_agents/rendu/AGENT.md:36` +— rôle « OTO Faisabilité Rendu 3D » **fait partie des `roles_allowed` du portail +`construction`** (droit d'invoquer le chat pour déclencher les rendus assistés) ; et +l'assertion **NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle +« OTO Plateforme Mobile » (famille `plateforme`) **volontairement dans AUCUNE +`roles_allowed`** (le chat ne couvre que les portails métier). Aucun gate ne les +couvrait : le bloc Chat OTOIA amont gate l'**identité** de la persona/capabilities +(ancrée CLAUDE.md) et la **composition** (5 mounts ⇔ 5 portails), aveugle à QUEL rôle +figure dans quelle `roles_allowed` ; le bloc Fiches Workspace gate un artefact + une +sémantique DIFFÉRENTS (accès console ≠ droit d'invocation du chat). On RECOMPUTE, depuis +`chat_mount.json`, l'ensemble des portails dont la `roles_allowed` porte +l'`erpnext_role_name` du rôle (résolu du contrat par `id`, zéro duplication · #6), et on +confronte l'assertion : **in_named_portail** (rendu) — le portail nommé est data-derived +(== le `portail` du rôle au contrat, **triple ancrage** : portail nommé == `portail` +contrat == unique portail dont la `roles_allowed` le porte) et membership == {ce portail} +ET la prose le cite avec le droit « invoquer le chat OTOIA » ; **not_in_any** (mobile) — +rôle ABSENT de TOUTE `roles_allowed`. Un portail nommé mais **absent** des mounts (typo) +échoue avant tout (anti-cible fantôme). 6 morsures vérifiées (3 côté artefact, 3 côté +fiche/bord) : artefact retire le rôle rendu de la `roles_allowed` construction (perte du +déclenchement assisté) · artefact ajoute le rôle mobile à `ventes` (**SUR-EXPOSITION** du +chat #6 VIOLÉE) · artefact ajoute AUSSI rendu à `compta` (droit élargi, la fiche nomme UN +seul portail) · fiche rendu nomme le mauvais portail (`construction→compta`, INTROUVABLE) · +fiche mobile retire le token `roles_allowed` de l'assertion négative (INTROUVABLE) ; +restauré = vert. + **Tokens design canoniques `publiciste/branding` ancrés sur CLAUDE.md #4** : la contrainte NON-NÉGOCIABLE #4 fixe l'identité visuelle luxury — fond dark `#0a0a12` + accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond**. Ces 4 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 80fa753..0187067 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -5215,6 +5215,118 @@ if _ws_json is not None and _ws_roles is not None: f"MAIS workspace.json met « {rname} » dans son Has Role → " f"SUR-EXPOSITION console, assertion #6 VIOLÉE") +# ============================================================================ +# Fiches agents · appartenance à `roles_allowed` du chat OTOIA — QUI peut +# INVOQUER l'assistant OTOIA embarqué dans un portail. Surface data-derived +# DISTINCTE de la membership Workspace `Has Role` (QUELLE console le rôle atteint, +# gatée juste au-dessus) ET des attributs du contrat rbac_50_roles.json (gatés par +# les blocs Fiches Faisabilité / Fiche Mobile) : la liste `roles_allowed` ne vit +# PAS dans le contrat ni dans workspace.json mais dans +# frontend/chat_otoia/out/chat_mount.json (une config runtime par portail, +# artefact byte-gaté par check_artifacts). Deux fiches font, EN PROSE, une +# assertion d'appartenance à CE contrôle d'accès : +# • rendu (:36) — POSITIVE : le rôle « OTO Faisabilité Rendu 3D » fait partie +# des `roles_allowed` du portail NOMMÉ `construction` (droit +# d'invoquer le chat pour déclencher les rendus assistés). +# • mobile (:41) — NÉGATIVE d'honnêteté (#6) : le rôle « OTO Plateforme Mobile » +# (famille `plateforme`) n'est VOLONTAIREMENT dans AUCUNE liste +# `roles_allowed` — le chat ne couvre que les portails métier. +# Aucun gate ne les couvrait : le bloc Chat OTOIA amont gate l'IDENTITÉ de la +# persona/capabilities (ancrée CLAUDE.md) et la COMPOSITION (5 mounts ⇔ 5 portails), +# AVEUGLE à QUEL rôle figure dans quelle `roles_allowed` ; le bloc Fiches Workspace +# gate la membership `Has Role`, un artefact + une sémantique DIFFÉRENTS (accès +# console ≠ droit d'invocation du chat). Ajouter le rôle mobile à une `roles_allowed` +# (SUR-EXPOSITION : le dev mobile gagnerait le droit de piloter l'IA d'un portail +# métier — l'escalade même que la note #6 refuse), retirer le rôle rendu de la +# `roles_allowed` construction (perte du déclenchement assisté), ou le DÉPLACER vers +# un autre portail, laisse la fiche périmée EN SILENCE pendant que l'artefact dit +# autre chose ⇒ l'agent ERPNext Backend câblerait le mauvais contrôle d'accès chat. +# Aucune suite tests/ (qui teste des FONCTIONS de montage/résolution, pas la prose +# d'une fiche) n'attrape ce « vert trompeur ». On RECOMPUTE, depuis chat_mount.json, +# l'ensemble des portails dont la `roles_allowed` porte l'erpnext_role_name du rôle +# (résolu du contrat par id, zéro duplication) et on confronte l'assertion de la +# fiche. Triple ancrage bonus (POSITIVE) : le portail NOMMÉ par la fiche == le +# `portail` du rôle dans le contrat == l'unique portail dont la `roles_allowed` +# le porte. Un claim absent échoue AUSSI (traçabilité · #6). +# ============================================================================ +FICHES_CHAT = [ + {"file": "03_agents/rendu/AGENT.md", "rid": "faisabilite-rendu-3d", + "kind": "in_named_portail"}, + {"file": "03_agents/mobile/AGENT.md", "rid": "plateforme-mobile", + "kind": "not_in_any"}, +] +_chat_mounts = _chat_roles = None +try: + _chat_mounts = load("frontend/chat_otoia/out/chat_mount.json") + _chat_roles = load("rbac/rbac_50_roles.json")["roles"] +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Fiches Chat · chat_mount.json / rbac_50_roles.json illisible : {e}") +if _chat_mounts is not None and _chat_roles is not None: + # Index recomputé : erpnext_role_name -> ensemble des portails dont la + # `roles_allowed` le porte (le droit d'invoquer le chat de CE portail). + _allow, _portails = {}, set() + for _mnt in _chat_mounts: + _pt = _mnt.get("portail") + _portails.add(_pt) + for _rn in (_mnt.get("roles_allowed") or []): + _allow.setdefault(_rn, set()).add(_pt) + if not _chat_mounts: + bad("Fiches Chat · aucun mount dans chat_mount.json → artefact suspect") + for fc in FICHES_CHAT: + tag = fc["file"].split("/")[1] + cand = [r for r in _chat_roles if r.get("id") == fc["rid"]] + if len(cand) != 1: + bad(f"Fiche {tag} · le contrat porte {len(cand)} rôle(s) `{fc['rid']}` " + f"(attendu exactement 1) → contrat changé de forme, vérifier") + continue + r0 = cand[0] + rname = r0.get("erpnext_role_name") + actual = _allow.get(rname, set()) # portails réels autorisant ce rôle + ftxt = read_norm(fc["file"]) + if ftxt is None: + continue + if fc["kind"] == "in_named_portail": + # Le portail NOMMÉ par la fiche est data-derived : c'est le `portail` du + # rôle dans le contrat (triple ancrage). Il DOIT exister comme mount + # (anti-typo : sinon l'assertion « verte » viserait un portail fantôme). + named = r0.get("portail") + if named not in _portails: + bad(f"Fiche {tag} · le rôle `{fc['rid']}` a portail `{named}` (contrat) " + f"MAIS aucun mount chat pour ce portail (mounts : {sorted(_portails)}) " + f"→ contrat/artefact périmé") + continue + # La fiche doit NOMMER ce portail dans son assertion roles_allowed + chat. + if not re.search(rf"`roles_allowed` \(portail {re.escape(named)}\)", ftxt): + bad(f"Fiche {tag} · {fc['file']} — assertion « `roles_allowed` (portail " + f"{named}) » INTROUVABLE (claim absent = régression · #6)") + elif not re.search(r"autorisés à \*\*invoquer le chat OTOIA\*\*", ftxt): + bad(f"Fiche {tag} · {fc['file']} — la fiche nomme le portail mais pas " + f"le droit « invoquer le chat OTOIA » → assertion incomplète (#6)") + elif actual == {named}: + good(f"Fiche {tag} · rôle « {rname} » ∈ roles_allowed du portail " + f"`{named}` (et de lui seul) == chat_mount.json") + elif named in actual: + bad(f"Fiche {tag} · rôle « {rname} » ∈ roles_allowed de `{named}` MAIS " + f"AUSSI {sorted(actual - {named})} — la fiche nomme UN seul portail ; " + f"droit d'invocation élargi dans chat_mount.json → réviser fiche/artefact") + else: + bad(f"Fiche {tag} · {fc['file']} nomme le portail `{named}` MAIS " + f"chat_mount.json autorise « {rname} » sur " + f"{sorted(actual) or 'aucun portail'} → fiche périmée (rôle " + f"retiré/déplacé de roles_allowed), régénérer") + elif fc["kind"] == "not_in_any": + if not re.search(r"volontairement pas\*\* rattaché.*?`roles_allowed` du chat " + r"OTOIA", ftxt): + bad(f"Fiche {tag} · {fc['file']} — assertion négative « volontairement pas " + f"… `roles_allowed` du chat OTOIA » INTROUVABLE (#6)") + elif not actual: + good(f"Fiche {tag} · rôle « {rname} » ABSENT de TOUTE roles_allowed " + f"== chat_mount.json (assertion négative #6 tenue)") + else: + bad(f"Fiche {tag} · {fc['file']} affirme « pas dans roles_allowed » MAIS " + f"chat_mount.json autorise « {rname} » sur {sorted(actual)} → " + f"SUR-EXPOSITION du chat OTOIA, assertion #6 VIOLÉE") + # ============================================================================ # Publiciste/branding · TOKENS DESIGN CANONIQUES « ancrés sur CLAUDE.md #4 » # — les 4 valeurs de la marque luxury (2 couleurs + 2 typographies), jamais gatées.