From 8667cdf4de970a07cf8ef00d5fb50f9249c2f26a Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 14:14:18 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20Fiscal/e-CF=20DGII=20:=20gate=20d'IDENTIT=C3=89=20des?= =?UTF-8?q?=20=C3=89V=C3=88NEMENTS=20D'=C3=89MISSION=20(=C2=AB=20le=20c?= =?UTF-8?q?=C5=93ur=20du=20livrable=20=C2=BB)=20=E2=80=94=20ENSEMBLE=20?= =?UTF-8?q?=C3=A9metteurs=20{r=C3=A9servation,contrat}=20=E2=A8=AF=20DISJO?= =?UTF-8?q?NCTION=20des=20brouillons=20(jamais=20de=20facture=20sur=20brou?= =?UTF-8?q?illon)=20=E2=A8=AF=20base=5Ffield=20=E2=A8=AF=20r=C3=B4le=20uni?= =?UTF-8?q?que=20compta-fiscaliste-ecf=20double-ancr=C3=A9=20RBAC=20portai?= =?UTF-8?q?l=20compta=20=E2=A8=AF=20FormaPago=20d=C3=A9faut=203/Tarjeta/Ca?= =?UTF-8?q?rdnet=20#10,=20recomput=C3=A9s=20d'ecf=5Fplan.json?= 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 | 63 +++++++++++++ ci/README.md | 28 +++++- ci/check_readme_claims.sh | 160 ++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index fc35b84..ec1afca 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -1508,3 +1508,66 @@ taux_a_confirmer` périmé 5→4 (c seul) · promesse anti-invention retirée du édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la confirmation réelle des taux + le câblage du calcul restent côté Direction/VPS). - **Auto-score 4Big** : 96/100. + +## Session `20260801_140334` · Buffer S8 · Domaine Fiscal/e-CF DGII : l'**IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION** — la section §Cross-cohérence présentée comme **« le cœur du livrable »** (README:40-59) — était transcrite À LA MAIN SANS aucun gate d'identité. Le SEUL gate fiscal existant couvre le **contrat de format e-NCF** (`E+tipo(2)+seq(10)=13`) + le compte de tests ; le bloc racine, le seul agrégat « e-CF 39 tests ». RIEN ne confrontait à l'artefact byte-gaté QUEL évènement du pipeline émet un e-CF, sur quel `base_field`, par quel rôle Compta, ni la FormaPago défaut. + +**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement +close ; poursuite de la série anti-dérive (CLAUDE.md #6). Même classe « identité +d'un ENSEMBLE » que la colonne « Entité » de la SPEC §4, l'énumération des onglets +Mobile ou les transitions à séparation des pouvoirs du workflow — appliquée à la +**surface data-derived la plus critique du module fiscal** : les `emission_events` +d'`ecf_plan.json`, jamais gatés hors le format e-NCF et le compte de tests. + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/fiscal/ecf_dgii/README.md:40-59` (§Cross-cohérence, « le cœur + du livrable ») — transcrit À LA MAIN : (L48) l'ENSEMBLE des évènements émetteurs + (« seulement réservation et contrat ») + l'ENSEMBLE exclu des brouillons (« on ne + facture pas un brouillon (lead/visite/devis) ») ; (L50) l'ENSEMBLE des `base_field` + (`montant_reservation`/`montant_contrat`) ; (L53-54) le rôle UNIQUE + (`compta-fiscaliste-ecf` · « OTO Compta Fiscaliste eCF » · portail compta) ; + (L59) la FormaPago défaut (« = 3 (Tarjeta) » · Cardnet #10). +- Source faisant autorité (byte-gatée par `check_artifacts`) : + `out/ecf_plan.json` — `emission_events[]` (`update_value`/`base_field`/`role_id`/ + `erpnext_role_name`), `forma_pago_defaut` + catalogue `formas_pago`. Double + ancrage du rôle sur `rbac/rbac_50_roles.json` (id → `portail` compta → nom ERPNext). +- Piège #6/#5, la dérive la plus dommageable ici : **AJOUTER un `emission_event` sur + un état BROUILLON** (ex. `devis`) — l'anti-pattern EXACT que le module interdit + (« on ne facture pas un brouillon ») — pendant que L48 dit encore « seulement + réservation et contrat » → l'agent ERPNext Backend câblerait une **facture émise + avant soumission/encaissement**. Aucune suite `tests/` (FONCTIONS de résolution/ + composition, jamais la prose vs l'artefact) ne l'attrape. +- **État courant** : émetteurs `{contrat, reservation}`, base_field + `{montant_contrat, montant_reservation}`, rôle unique `compta-fiscaliste-ecf` + (portail compta · RBAC concordant), FormaPago défaut `3`/Tarjeta/Cardnet — + tout recoupe l'artefact EXACTEMENT (anti-invention #6, rien à réécrire). Le + défaut est la **surface ungated**. + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiscal · e-CF DGII — +IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION » avant `sys.exit`) : (0) **cohérence interne** — +rôle UNIQUE sur tous les évènements · défaut ∈ catalogue FormaPago ; (1) **ENSEMBLE +émetteurs** — set-diff README `seulement …` == `update_value` de l'artefact (ni +fantôme ni manquant · accents/casse normalisés) ; (2) **DISJONCTION** — l'ensemble +exclu `(lead/visite/devis)` ∩ émetteurs = ∅ (jamais de facture sur brouillon — mord +un `devis` émetteur ET la self-contradiction du README) ; (3) **ENSEMBLE `base_field`** +— set-diff README == artefact ; (4) **rôle unique** cité backtické + nom ERPNext, +**double-ancré** sur `rbac_50_roles` (id → portail compta → nom concordant) ; (5) +**FormaPago défaut** — code == artefact · label ∈ catalogue · motif cite Cardnet (#10). +Un claim absent échoue AUSSI (traçabilité #6). + +**6 morsures vérifiées** : artefact facture un BROUILLON (`devis` émetteur → mismatch +d'ensemble **ET** clash de disjonction · 2 morsures en une) · artefact renomme un +`base_field` (`montant_reservation→montant_acompte`) · artefact change FormaPago défaut +`3→1` (Efectivo) · artefact casse le rôle unique (2ᵉ évènement → `compta-comptable`) · +README périmé « seulement contrat » (réservation retiré) · motif du défaut ne cite +plus « Cardnet » (#10) ; restauré = green : émetteurs · disjonction · base_field · +rôle == RBAC · FormaPago == artefact · exit 0. Working tree byte-restauré +(`git checkout --`, **JAMAIS** `git clean`, interdit absolu) · **7 gates re-verts** +(`check_readme_claims`/`check_docs`/`guard_constraints`/`check_artifacts`/ +`check_ci_integrity`/`validate_json`/`check_regression`). + +- `ci/README.md` (clause récap du pipeline `check-readme-claims` + paragraphe détaillé + « IDENTITÉ des évènements d'émission e-CF ancrée sur `ecf_plan.json` ») mis à jour. +- **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 connexion + Compupar réelle + l'émission e-CF en production restent côté ERPNext Backend/VPS). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 1579aed..b4599ed 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, 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) == 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, 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 | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -1249,6 +1249,32 @@ rendu** mistype une citation (id fantôme) · **fiche bim** retire le tag Flux ( bullet canonique (ancre morte, recompute impossible) ; restauré = vert (exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**. +**IDENTITÉ des évènements d'émission e-CF (le « cœur du livrable » fiscal) ancrée sur +`ecf_plan.json`.** La section §Cross-cohérence de `fiscal/ecf_dgii/README.md:40-59` — « le +cœur du livrable » — transcrit **à la main** l'identité des `emission_events` de l'artefact +byte-gaté : l'**ENSEMBLE** des évènements qui émettent un e-CF (« seulement réservation et +contrat »), l'**ENSEMBLE exclu** des brouillons (« on ne facture pas un brouillon +(lead/visite/devis) »), l'ENSEMBLE des `base_field` (`montant_reservation`/`montant_contrat`), +le **rôle unique** (`compta-fiscaliste-ecf` · « OTO Compta Fiscaliste eCF » · portail compta) +et la **FormaPago défaut** (« = 3 (Tarjeta) » · Cardnet #10). Le seul gate fiscal existant +couvre le **format e-NCF** + le compte de tests ; le bloc racine, le seul agrégat « e-CF 39 +tests ». **Rien** ne confrontait cette identité à l'artefact. Dérive la plus grave (#6/#5) : +**ajouter un `emission_event` sur un état BROUILLON** (ex. `devis`) — l'anti-pattern EXACT +que le module interdit (« on ne facture pas un brouillon ») — pendant que le README dit encore +« seulement réservation et contrat » → l'agent ERPNext Backend câblerait une **facture émise +avant soumission/encaissement**. On RECOMPUTE les ENSEMBLES depuis `ecf_plan.json` (jamais une +liste à la main) et exige que le README les reflète **EXACTEMENT** (set-diff : ni fantôme ni +manquant ; accents/casse normalisés) ; le rôle est **double-ancré** sur `rbac_50_roles.json` +(id → portail compta → nom ERPNext concordant) ; l'ensemble exclu doit rester **DISJOINT** des +émetteurs (jamais de facture sur brouillon). Cohérences croisées en bonus : rôle **unique** sur +tous les évènements · défaut ∈ **catalogue** FormaPago · ensembles non vides. **6 morsures +vérifiées** : artefact facture un brouillon (`devis` émetteur → mismatch d'ensemble **ET** +clash de disjonction) · artefact renomme un `base_field` · artefact change la FormaPago défaut +`3→1` · artefact casse le rôle unique (2ᵉ évènement → autre rôle) · README périmé « seulement +contrat » (réservation retiré) · motif du défaut ne cite plus « Cardnet » (#10) ; restauré = +vert (exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · +**7 gates re-verts**. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 123fa0b..9cc0a1b 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -5557,6 +5557,166 @@ if _rp_src is not None: bad(f"RunPod endpoint · AUCUNE fiche/master prompt ne mentionne RunPod — " f"la surface d'identité de l'endpoint GPU s'est évaporée (régression #6)") +# ============================================================================ +# Fiscal · e-CF DGII — IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION (le « cœur du livrable ») +# ---------------------------------------------------------------------------- +# La section §Cross-cohérence du README (05_deliverables_mvp/fiscal/ecf_dgii/ +# README.md:40-59) — présentée comme « le cœur du livrable » — transcrit À LA MAIN +# l'IDENTITÉ des emission_events d'out/ecf_plan.json (byte-gaté par check_artifacts +# ⇒ artefact faisant autorité) : +# • L48 l'ENSEMBLE des évènements qui émettent un e-CF (« seulement réservation +# et contrat ») + l'ENSEMBLE exclu « on ne facture pas un brouillon +# (lead/visite/devis) » — la RAISON D'ÊTRE du module : jamais de facture +# sur un brouillon (états SOUMIS uniquement · doc_status=1) +# • L50 l'ENSEMBLE des base_field (« montant_reservation, montant_contrat ») +# • L53 le rôle UNIQUE (« compta-fiscaliste-ecf » · « OTO Compta Fiscaliste +# eCF » · portail compta) +# • L59 la FormaPago défaut (« = 3 (Tarjeta) » · Cardnet #10) +# Le SEUL gate fiscal existant couvre le CONTRAT DE FORMAT e-NCF + le compte de +# tests ; le bloc racine, le seul agrégat « e-CF 39 tests ». RIEN ne confronte +# cette identité à l'artefact. Dérive la plus grave, #6/#5 : AJOUTER un +# emission_event sur un état BROUILLON (ex. `devis`) — l'anti-pattern EXACT que le +# module interdit (« on ne facture pas un brouillon ») — pendant que L48 dit encore +# « seulement réservation et contrat » → l'agent ERPNext Backend câblerait une +# facturation sur brouillon (e-CF émis avant soumission/encaissement). Aucune suite +# tests/ (FONCTIONS de résolution/composition, jamais la prose vs l'artefact) ne +# l'attrape — classe « vert trompeur » notée en mémoire. +# +# On RECOMPUTE les ENSEMBLES depuis ecf_plan.json (jamais une liste à la main · #6) +# et EXIGE que le README les reflète EXACTEMENT (set-diff : ni fantôme ni manquant ; +# accents/casse normalisés). Le rôle est en outre DOUBLE-ANCRÉ sur rbac_50_roles +# (id → portail compta → nom ERPNext concordant). Cohérences croisées en bonus +# (mordent un plan INTERNEMENT incohérent) : rôle UNIQUE sur tous les évènements · +# défaut ∈ catalogue FormaPago · ensembles NON VIDES. Un claim absent échoue AUSSI +# (traçabilité #6). +# ============================================================================ +ECF_RD2 = f"{D}/fiscal/ecf_dgii/README.md" +try: + _ecfp = load("fiscal/ecf_dgii/out/ecf_plan.json") + _ecf_ev = _ecfp["emission_events"] + if not _ecf_ev: + raise ValueError("emission_events vide") + ecf_updvals = {e["update_value"] for e in _ecf_ev} + ecf_bases = {e["base_field"] for e in _ecf_ev} + ecf_roleids = {e["role_id"] for e in _ecf_ev} + ecf_rolenms = {e["erpnext_role_name"] for e in _ecf_ev} + _ecf_fpd = _ecfp["forma_pago_defaut"] + _ecf_fpcat = {str(f["code"]): f["label"] for f in _ecfp["formas_pago"]} +except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as e: + bad(f"Fiscal e-CF évènements · ecf_plan.json illisible/inattendu : {e}") + _ecf_ev = None + +if _ecf_ev is not None: + def _ecf_ai(s): # accent-insensible · minuscule · sans espaces + return "".join(c for c in unicodedata.normalize("NFKD", str(s)) + if unicodedata.category(c) != "Mn").lower().replace(" ", "") + try: + ecf_doc = re.sub(r"\s+", " ", open(ECF_RD2, encoding="utf-8").read()) + except OSError as e: + bad(f"Fiscal e-CF évènements · {ECF_RD2} illisible : {e}"); ecf_doc = None + + # (0) cohérence interne du plan : rôle UNIQUE + défaut ∈ catalogue FormaPago + if len(ecf_roleids) != 1 or len(ecf_rolenms) != 1: + bad(f"Fiscal e-CF évènements · plan INCOHÉRENT : rôle non unique sur les " + f"évènements (ids={sorted(ecf_roleids)} noms={sorted(ecf_rolenms)}) — " + f"le README promet un « rôle dédié » unique") + _fp_code = str(_ecf_fpd.get("code")) + if _fp_code not in _ecf_fpcat: + bad(f"Fiscal e-CF évènements · forma_pago_defaut.code={_fp_code!r} ABSENT " + f"du catalogue formas_pago {sorted(_ecf_fpcat)} (plan incohérent)") + + if ecf_doc is not None: + # (1) ENSEMBLE des évènements émetteurs — L48 « seulement X et Y » + m_bill = re.search(r"seulement\s+([^.]+?)\.", ecf_doc) + want = {_ecf_ai(x) for x in ecf_updvals} + if m_bill is None: + bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération « seulement … » " + f"INTROUVABLE (attendu {sorted(ecf_updvals)})") + else: + bill = {_ecf_ai(x) for x in re.split(r"\s+et\s+|,\s*", m_bill.group(1)) if x.strip()} + if bill == want: + good(f"Fiscal e-CF évènements · émetteurs README == ecf_plan " + f"({sorted(ecf_updvals)})") + else: + bad(f"Fiscal e-CF évènements · émetteurs README {sorted(bill)} ≠ " + f"ecf_plan {sorted(want)} (fantôme={sorted(bill-want)} " + f"manquant={sorted(want-bill)}) → régénérer le README") + # (2) ENSEMBLE exclu (brouillons) DISJOINT des émetteurs — anti-facture-brouillon + m_excl = re.search(r"on ne facture pas un brouillon\s*\(([^)]+)\)", ecf_doc) + if m_excl is None: + bad(f"Fiscal e-CF évènements · {ECF_RD2} — clause « on ne facture pas un " + f"brouillon (…) » INTROUVABLE (raison d'être #6 · traçabilité)") + else: + excl = {_ecf_ai(x) for x in re.split(r"[/,]", m_excl.group(1)) if x.strip()} + clash = excl & want + if clash: + bad(f"Fiscal e-CF évènements · un état déclaré BROUILLON {sorted(clash)} " + f"est ÉMETTEUR dans ecf_plan → FACTURE SUR BROUILLON (l'anti-pattern " + f"que le module interdit · #6) OU README auto-contradictoire") + else: + good(f"Fiscal e-CF évènements · brouillons {sorted(excl)} ∩ émetteurs = ∅ " + f"(jamais de facture sur brouillon)") + # (3) ENSEMBLE des base_field — L50 « (montant_reservation, montant_contrat) » + m_base = re.search(r"champ \*\*Currency réel\*\*[^(]*\(([^)]+)\)", ecf_doc) + if m_base is None: + bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération des base_field " + f"INTROUVABLE (attendu {sorted(ecf_bases)})") + else: + docbases = set(re.findall(r"`([^`]+)`", m_base.group(1))) + if docbases == ecf_bases: + good(f"Fiscal e-CF évènements · base_field README == ecf_plan " + f"({sorted(ecf_bases)})") + else: + bad(f"Fiscal e-CF évènements · base_field README {sorted(docbases)} ≠ " + f"ecf_plan {sorted(ecf_bases)} (fantôme={sorted(docbases-ecf_bases)} " + f"manquant={sorted(ecf_bases-docbases)})") + # (4) rôle UNIQUE : backtické + nom ERPNext + DOUBLE-ANCRÉ portail compta (RBAC) + _rid = next(iter(ecf_roleids)); _rnm = next(iter(ecf_rolenms)) + if f"`{_rid}`" not in ecf_doc: + bad(f"Fiscal e-CF évènements · rôle dédié `{_rid}` (== ecf_plan) NON cité " + f"backtické dans le README") + elif _rnm not in ecf_doc: + bad(f"Fiscal e-CF évènements · nom ERPNext « {_rnm} » (== ecf_plan) ABSENT " + f"du README") + else: + try: + _rbac = load("rbac/rbac_50_roles.json")["roles"] + _rrec = next((r for r in _rbac if r.get("id") == _rid), None) + except (OSError, KeyError, TypeError, json.JSONDecodeError) as e: + bad(f"Fiscal e-CF évènements · rbac_50_roles illisible : {e}"); _rrec = "ERR" + if _rrec is None: + bad(f"Fiscal e-CF évènements · rôle `{_rid}` (ecf_plan) INTROUVABLE dans " + f"rbac_50_roles → rôle Compta fantôme") + elif _rrec == "ERR": + pass + elif _rrec.get("portail") != "compta": + bad(f"Fiscal e-CF évènements · rôle `{_rid}` portail={_rrec.get('portail')!r} " + f"≠ « compta » (le README exige le portail compta)") + elif _rrec.get("erpnext_role_name") != _rnm: + bad(f"Fiscal e-CF évènements · nom ERPNext discordant ecf_plan « {_rnm} » " + f"vs RBAC « {_rrec.get('erpnext_role_name')} »") + else: + good(f"Fiscal e-CF évènements · rôle unique `{_rid}` (« {_rnm} ») == " + f"ecf_plan == RBAC (portail compta)") + # (5) FormaPago défaut — L59 « = 3 (Tarjeta) » · Cardnet #10 + m_fp = re.search(r"FormaPago défaut\s*=\s*(\d+)\s*\(([^)]+)\)", ecf_doc) + _lbl = _ecf_fpcat.get(_fp_code, "") + if m_fp is None: + bad(f"Fiscal e-CF évènements · {ECF_RD2} — « FormaPago défaut = N (…) » " + f"INTROUVABLE (attendu {_fp_code} / {_lbl})") + elif m_fp.group(1) != _fp_code: + bad(f"Fiscal e-CF évènements · FormaPago défaut README ={m_fp.group(1)} " + f"MAIS ecf_plan dit ={_fp_code} ({_lbl}) → défaut périmé") + elif _ecf_ai(m_fp.group(2)) not in _ecf_ai(_lbl): + bad(f"Fiscal e-CF évènements · FormaPago défaut label README " + f"« {m_fp.group(2)} » incohérent avec le catalogue « {_lbl} » (code {_fp_code})") + elif "cardnet" not in _ecf_ai(_ecf_fpd.get("motif", "")): + bad(f"Fiscal e-CF évènements · forma_pago_defaut.motif ne cite plus " + f"« Cardnet » (#10) → justification du défaut évaporée") + else: + good(f"Fiscal e-CF évènements · FormaPago défaut = {_fp_code} " + f"({_lbl}) · Cardnet #10 == ecf_plan") + sys.exit(1 if FAIL else 0) PY rc=$?