From 15812c1dff865918aae5bac37b5a1435fe0c2116 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 23:13:00 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260801=5F230453]=20Sprint=208?= =?UTF-8?q?=20=C2=B7=20buffer=20=C2=B7=20QA/Audit=205D=20:=20gate=20d'IDEN?= =?UTF-8?q?TIT=C3=89=20du=20VERDICT=20GLOBAL=20=E2=80=94=20le=20LABEL=20de?= =?UTF-8?q?=20conclusion=20de=20l'audit=20(PASS=20/=20PASS=5FWITH=5FOPEN?= =?UTF-8?q?=5FITEMS=20/=20FAIL),=20la=20HEADLINE=20du=20livrable,=20=C3=A9?= =?UTF-8?q?tait=20HORS=20de=20tout=20gate=20:=20le=20bloc=20audit=5F5d=20e?= =?UTF-8?q?xistant=20recompute=20la=20ventilation=20chiffr=C3=A9e=20(13=20?= =?UTF-8?q?PASS=20=C2=B7=200=20FAIL=20=C2=B7=204=20=C3=A0=20confirmer),=20?= =?UTF-8?q?les=20open=20items,=20les=205=20dimensions=20et=20les=20livrabl?= =?UTF-8?q?es=20audit=C3=A9s=20=E2=80=94=20jamais=20le=20label=20lui-m?= =?UTF-8?q?=C3=AAme.=20Or=20il=20est=20data-derived=20(audit=5Freport.json?= =?UTF-8?q?[verdict]=20=3D=3D=20MANIFEST.json[verdict],=20byte-gat=C3=A9s)?= =?UTF-8?q?,=20recalcul=C3=A9=20par=20builder.py=20(FAIL=20si=20=E2=89=A51?= =?UTF-8?q?=20FAIL=20=C2=B7=20sinon=20PASS=5FWITH=5FOPEN=5FITEMS=20si=20?= =?UTF-8?q?=E2=89=A51=20A=5FCONFIRMER=20=C2=B7=20sinon=20PASS)=20et=20tran?= =?UTF-8?q?scrit=20=C3=A0=20la=20main=20=C3=A0=202=20endroits=20du=20READM?= =?UTF-8?q?E=20(la=20R=C3=88GLE=20l.39=20+=20le=20VERDICT=20COURANT=20l.49?= =?UTF-8?q?).=20Nouveau=20bloc=20:=20on=20MIROITE=20la=20r=C3=A8gle=20et?= =?UTF-8?q?=20on=20exige=20(a)=20HONN=C3=8ATET=C3=89=20interne=20audit=5Fr?= =?UTF-8?q?eport.verdict=20=3D=3D=20verdict(triplet)=20=E2=80=94=20un=20PA?= =?UTF-8?q?SS=20forc=C3=A9=20pendant=20que=204=20params=20restent=20a=5Fco?= =?UTF-8?q?nfirmer=20(C=C5=92UR=20#6=20:=20ne=20jamais=20forcer=20un=20PAS?= =?UTF-8?q?S)=20est=20mordu=20;=20(b)=20MANIFEST.verdict=20=3D=3D=20audit?= =?UTF-8?q?=5Freport.verdict=20;=20(c)=20label=20=C2=AB=20Verdict=20couran?= =?UTF-8?q?t=20=C2=BB=20README=20=3D=3D=20artefact=20;=20(d)=20la=20r?= =?UTF-8?q?=C3=A8gle=20=C2=AB=20Verdict=20global=20=C2=BB=20=C3=A9num?= =?UTF-8?q?=C3=A8re=20EXACTEMENT=20le=20codomaine=20{FAIL,=20PASS=5FWITH?= =?UTF-8?q?=5FOPEN=5FITEMS,=20PASS}=20du=20g=C3=A9n=C3=A9rateur.=205=20mor?= =?UTF-8?q?sures=20v=C3=A9rifi=C3=A9es=20(2=20README=20=C2=B7=202=20artefa?= =?UTF-8?q?ct=20+=20M3=20cascade=20a+b+c=20=C2=B7=201=20disparition),=207?= =?UTF-8?q?=20gates=20re-verts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-01.md | 60 ++++++++++++++++++++++++ ci/README.md | 2 +- ci/check_readme_claims.sh | 88 +++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index b2b02ff..7b2edb8 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -1,5 +1,65 @@ # Activity Log · 2026-08-01 · Claude Code DTP +## Session `20260801_230453` · Buffer S8 · QA/Audit 5D : gate d'IDENTITÉ du **VERDICT GLOBAL** — le LABEL de conclusion de l'audit + +**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement +complète ; la valeur marginale reste le durcissement anti-dérive (CLAUDE.md #6) +des surfaces prose data-derived encore non gatées. Les 7 gates verts au départ. + +**Surface trouvée (aveugle)** : le **verdict global** de l'audit 5D — le LABEL +`PASS` / `PASS_WITH_OPEN_ITEMS` / `FAIL` qui est la **HEADLINE de tout le +livrable** — était **HORS de tout gate**. Le bloc audit_5d existant recompute la +**ventilation chiffrée** (« **13 PASS · 0 FAIL · 4 à confirmer** », pass/fail/à +confirmer d'`audit_report.totals`), l'énumération des open items, les 5 dimensions +et les livrables audités — mais **jamais le label de verdict lui-même**. Or ce +label est data-derived (`out/audit_report.json[verdict]` == `out/MANIFEST.json[verdict]` +== `PASS_WITH_OPEN_ITEMS`, byte-gatés), recalculé par `qalib/builder.py` via la +règle **`FAIL` si ≥1 FAIL · sinon `PASS_WITH_OPEN_ITEMS` si ≥1 A_CONFIRMER · sinon +`PASS`**, et transcrit à la main à **deux** endroits du README : la RÈGLE +(`Verdict global : …`, l.39) et le VERDICT COURANT (`## Verdict courant` l.49). Le +risque #6 est le **CŒUR** du livrable : écrire `PASS` alors que 4 params +réglementaires restent `a_confirmer` = l'audit « force » un PASS et prétend propre +un dossier qui ne l'est pas — exactement ce que l'audit reproche aux autres. La +ventilation chiffrée peut rester exacte pendant que le label ment ; ni +`check_artifacts` (byte only) ni les suites `tests/` (FONCTIONS, pas la prose) +n'attrapent. + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Audit 5D · IDENTITÉ du VERDICT +GLOBAL », inséré juste après la ventilation chiffrée) : on **MIROITE** la règle du +générateur et on exige — **(a)** HONNÊTETÉ interne : `audit_report.verdict` == +verdict recalculé du triplet (fail>0→FAIL · a_confirmer>0→PASS_WITH_OPEN_ITEMS · +sinon PASS) ⇒ un verdict FORCÉ incohérent avec ses propres chiffres est mordu ; +**(b)** CROISÉ : `MANIFEST.verdict` == `audit_report.verdict` ; **(c)** le LABEL +« Verdict courant » du README == `audit_report.verdict` (ancré sur le heading + +la ventilation `**N PASS`, pas sur la règle l.39) ; **(d)** la RÈGLE « Verdict +global » énumère EXACTEMENT le **codomaine** `{FAIL, PASS_WITH_OPEN_ITEMS, PASS}` +du générateur (un label renommé côté `builder.py` sans mise à jour de la règle est +mordu). Claim absent = échec (disparition = régression). + +**5 morsures vérifiées** (working tree byte-restauré après chacune via +`git checkout -- `, JAMAIS `git checkout .` / `git clean`) : +1. README label `PASS_WITH_OPEN_ITEMS`→`PASS` → ✗ (c) « Verdict courant = PASS MAIS + artefact PASS_WITH_OPEN_ITEMS » ; +2. README règle l.39 `PASS_WITH_OPEN_ITEMS`→`PASS_OPEN` → ✗ (d) « codomaine + [FAIL, PASS, PASS_OPEN] ≠ [FAIL, PASS, PASS_WITH_OPEN_ITEMS] » ; +3. `audit_report.verdict`→`PASS` (à_confirmer reste 4) → ✗ (a) « verdict FORCÉ, + incohérent — viole #6 » **+** cascade ✗ (b) + ✗ (c) (3 fails : l'honnêteté est + la morsure la plus forte) ; +4. `MANIFEST.verdict`→`PASS` → ✗ (b) « MANIFEST PASS MAIS audit_report + PASS_WITH_OPEN_ITEMS (désynchronisés) » ; +5. README label supprimé (l.49) → ✗ (c) « Verdict courant INTROUVABLE ». +Restauré = green : (a)(b)(c)(d) tous ✓. + +- `ci/README.md` : la sous-clause « audit 5D » de la row `check-readme-claims` + étendue de l'IDENTITÉ du VERDICT GLOBAL (label + honnêteté interne + croisé + MANIFEST + codomaine de la règle documentée). +- **7 gates re-verts** : `guard_constraints` / `validate_json` / `check_docs` / + `check_artifacts` / `check_regression` / `check_ci_integrity` / + `check_readme_claims` (tous rc=0). +- **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). +- **Auto-score 4Big** : 96/100. + ## Session `20260801_210434` · Buffer S8 · Domaine CRM/commissions : l'**IDENTITÉ du CHAMP DEVISE** — la clause `devise_field` = le champ `devise` (`Select` **USD/DOP** · #10) du DocType Dossier Vente, transcrite **en prose** dans la §« Cross-cohérence » (`crm/commissions/README.md:54`) — était **HORS de tout gate**. Les trois blocs commissions déjà en place gatent les états soumis / brouillons exclus, les champs de base Currency, l'identité des rôles payés (portail `ventes`) et la promesse anti-taux #6 — mais **aucun** ne touchait ce couple champ↔devises (l'**invariant CLI #8** du générateur). **Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement diff --git a/ci/README.md b/ci/README.md index 9b17f91..34abae1 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** du COMPTE de suites gated de la fiche devops (cellule « N suites gated » de la row pipeline CI — un compte de SUITES, classe distincte des comptes de tests, recomputé de `regression_plan.totals.suites` + cohérence croisée == `regression_run` len(suites), plan↔run) **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 COMPTE d'INVARIANTS CI du générateur SEO (compte annoncé « ## Garanties (N invariants) » du README + toutes ses mentions « N invariants » + les 5 self-reports du `.py` — docstrings/aides/succès CLI — recomputés du LEDGER numéroté `# N ·` de `_validate`, contiguïté 1..N ; classe distincte des comptes d'artefact) **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` + IDENTITÉ des LIVRABLES AUDITÉS — la liste des hand-off `out/*.json` que l'audit de second niveau LIT comme matière première, énumérée à la main dans la section « ## Livrables audités » du README, recomputée par ENSEMBLE de `MANIFEST.audited_artifacts` byte-gaté (ordre README ≠ ordre MANIFEST trié) : puces README == registre (absent ET fantôme mordus) + cohérences croisées : chaque chemin audité RÉSOUT un fichier commité (le `_load` de l'auditeur lève sinon — audit à vide impossible) · |livrables audités| == |alias distincts d'artefacts référencés par les `controls[].artifacts` du spec| (artefact chargé jamais contrôlé, ou alias contrôlé sans chemin de chargement, désynchronise) ; brancher/retirer un livrable amont (`ARTIFACT_PATHS`) périme la section en silence ; section absente échoue AUSSI) **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 + IDENTITÉ de la LISTE D'INVARIANTS CI de la section « ## Invariants (N) — garantis en CI » du README : compte annoncé, énumération numérotée en prose ET self-report CLI « N invariants verts » du générateur, tous recomputés du REGISTRE-source = marqueurs `# Invariant N` de `chat_otoia_gen.py` — set-diff absent/fantôme + registre code contigu 1..N) **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** de l'**identité du champ devise** du même livrable (le « sur quelle devise » · invariant #8 : `plan.devise_field == "devise"` == le champ `devise` du DocType Dossier Vente, fieldtype `Select`, `options` ORDONNÉES == les 2 devises canoniques de CLAUDE.md #10 « **USD + DOP** », + la prose README:54 « `devise` (`Select` USD/DOP · #10) » recomputée) **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 | +| `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** du COMPTE de suites gated de la fiche devops (cellule « N suites gated » de la row pipeline CI — un compte de SUITES, classe distincte des comptes de tests, recomputé de `regression_plan.totals.suites` + cohérence croisée == `regression_run` len(suites), plan↔run) **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 COMPTE d'INVARIANTS CI du générateur SEO (compte annoncé « ## Garanties (N invariants) » du README + toutes ses mentions « N invariants » + les 5 self-reports du `.py` — docstrings/aides/succès CLI — recomputés du LEDGER numéroté `# N ·` de `_validate`, contiguïté 1..N ; classe distincte des comptes d'artefact) **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 + IDENTITÉ du VERDICT GLOBAL — le LABEL de conclusion (`PASS`/`PASS_WITH_OPEN_ITEMS`/`FAIL`) recomputé de la règle du générateur : honnêteté interne `audit_report.verdict == verdict(triplet)` (jamais de PASS forcé · #6), croisé `MANIFEST.verdict`, LABEL « Verdict courant » du README, et codomaine `{FAIL, PASS_WITH_OPEN_ITEMS, PASS}` de la règle « Verdict global » documentée + é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` + IDENTITÉ des LIVRABLES AUDITÉS — la liste des hand-off `out/*.json` que l'audit de second niveau LIT comme matière première, énumérée à la main dans la section « ## Livrables audités » du README, recomputée par ENSEMBLE de `MANIFEST.audited_artifacts` byte-gaté (ordre README ≠ ordre MANIFEST trié) : puces README == registre (absent ET fantôme mordus) + cohérences croisées : chaque chemin audité RÉSOUT un fichier commité (le `_load` de l'auditeur lève sinon — audit à vide impossible) · |livrables audités| == |alias distincts d'artefacts référencés par les `controls[].artifacts` du spec| (artefact chargé jamais contrôlé, ou alias contrôlé sans chemin de chargement, désynchronise) ; brancher/retirer un livrable amont (`ARTIFACT_PATHS`) périme la section en silence ; section absente échoue AUSSI) **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 + IDENTITÉ de la LISTE D'INVARIANTS CI de la section « ## Invariants (N) — garantis en CI » du README : compte annoncé, énumération numérotée en prose ET self-report CLI « N invariants verts » du générateur, tous recomputés du REGISTRE-source = marqueurs `# Invariant N` de `chat_otoia_gen.py` — set-diff absent/fantôme + registre code contigu 1..N) **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** de l'**identité du champ devise** du même livrable (le « sur quelle devise » · invariant #8 : `plan.devise_field == "devise"` == le champ `devise` du DocType Dossier Vente, fieldtype `Select`, `options` ORDONNÉES == les 2 devises canoniques de CLAUDE.md #10 « **USD + DOP** », + la prose README:54 « `devise` (`Select` USD/DOP · #10) » recomputée) **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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 229479a..225e987 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -2272,6 +2272,94 @@ if a5_ctl is not None: bad(f"Audit 5D · {A5D_FI} — dit {'/'.join(m.groups())} MAIS artefact dit " f"{a5_ctl}/{a5_dim} ({a5_src}) → régénérer la fiche") +# ---------------------------------------------------------------------------- +# Audit 5D · IDENTITÉ du VERDICT GLOBAL — le LABEL de conclusion (`PASS` / +# `PASS_WITH_OPEN_ITEMS` / `FAIL`), pas seulement sa ventilation chiffrée. +# ---------------------------------------------------------------------------- +# Le bloc de ventilation ci-dessus recompute le TRIPLET chiffré (pass/fail/à +# confirmer) mais reste AVEUGLE au LABEL de verdict lui-même — la HEADLINE de tout +# l'audit. Ce label est data-derived (out/audit_report.json[verdict] == out/ +# MANIFEST.json[verdict], byte-gatés par check_artifacts ⇒ faisant autorité), +# recalculé par le générateur (qalib/builder.py) via la règle EXACTE : `FAIL` si +# ≥1 FAIL · sinon `PASS_WITH_OPEN_ITEMS` si ≥1 A_CONFIRMER · sinon `PASS`. Le README +# le transcrit À LA MAIN à DEUX endroits — la RÈGLE (« Verdict global : `FAIL` si … +# sinon `PASS_WITH_OPEN_ITEMS` … sinon `PASS` ») ET le VERDICT COURANT +# (« ## Verdict courant \n `PASS_WITH_OPEN_ITEMS` — **13 PASS · 0 FAIL · 4 à +# confirmer** »). AUCUN gate ne le vérifiait : le triplet peut coller à l'artefact +# pendant que le LABEL courant ment (écrire `PASS` alors que 4 open items +# subsistent = violation #6 « ne jamais forcer un PASS », le CŒUR même du livrable — +# l'audit prétend propre un dossier avec 4 params réglementaires non confirmés) ; +# ou la RÈGLE documentée dérive du codomaine réel du générateur (un label renommé +# dans builder.py, la prose périmée). check_artifacts ne prouve QUE +# audit_report==build (byte) ; aucune suite tests/ (FONCTIONS, pas la prose du +# README) n'attrape ces deux dérives. On MIROITE la règle du générateur et on +# exige : (a) HONNÊTETÉ interne — audit_report.verdict == verdict recalculé du +# triplet (l'audit ne peut afficher un verdict incohérent avec ses propres +# chiffres) ; (b) CROISÉ — MANIFEST.verdict == audit_report.verdict ; (c) le LABEL +# « Verdict courant » du README == audit_report.verdict ; (d) la RÈGLE du README +# énumère EXACTEMENT le codomaine {FAIL, PASS_WITH_OPEN_ITEMS, PASS} (un label +# renommé côté code sans mise à jour de la règle est mordu). Un claim absent échoue +# AUSSI (disparition = régression). +# ============================================================================ +A5V_DOMAIN = {"FAIL", "PASS_WITH_OPEN_ITEMS", "PASS"} +def _a5_verdict_from_totals(t): + # miroir EXACT de qalib/builder.py : FAIL prime, puis open items, sinon PASS + if t["fail"]: + return "FAIL" + if t["a_confirmer"]: + return "PASS_WITH_OPEN_ITEMS" + return "PASS" +try: + a5v_rep = load("qa/audit_5d/out/audit_report.json") + a5v_verdict = a5v_rep["verdict"] + a5v_tot = a5v_rep["totals"] + a5v_man_verdict = load("qa/audit_5d/out/MANIFEST.json")["verdict"] +except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Audit 5D · verdict — artefact illisible/inattendu : {e}"); a5v_verdict = None +if a5v_verdict is not None: + # (a) honnêteté interne : le verdict de l'artefact découle de ses propres totals + a5v_exp = _a5_verdict_from_totals(a5v_tot) + if a5v_verdict != a5v_exp: + bad(f"Audit 5D · verdict — audit_report dit « {a5v_verdict} » MAIS ses totals " + f"(fail={a5v_tot['fail']} · à_confirmer={a5v_tot['a_confirmer']}) imposent " + f"« {a5v_exp} » (verdict FORCÉ, incohérent — viole #6)") + else: + good(f"Audit 5D · verdict — audit_report « {a5v_verdict} » == recalcul du triplet " + f"(fail={a5v_tot['fail']} · à_confirmer={a5v_tot['a_confirmer']})") + # (b) croisé artefacts : MANIFEST == rapport + if a5v_man_verdict != a5v_verdict: + bad(f"Audit 5D · verdict — MANIFEST dit « {a5v_man_verdict} » MAIS audit_report " + f"dit « {a5v_verdict} » (artefacts désynchronisés)") + else: + good(f"Audit 5D · verdict — MANIFEST « {a5v_man_verdict} » == audit_report") + a5v_rd = read_norm(A5D_RD) + if a5v_rd is not None: + # (c) LABEL « Verdict courant » du README == verdict de l'artefact + m = re.search(r"## Verdict courant\s+`([A-Z_]+)`\s*—\s*\*\*\d+ PASS", a5v_rd) + if m is None: + bad(f"Audit 5D · {A5D_RD} — label « Verdict courant » INTROUVABLE " + f"(attendu « {a5v_verdict} »)") + elif m.group(1) == a5v_verdict: + good(f"Audit 5D · {A5D_RD} — « Verdict courant » = « {m.group(1)} » == artefact") + else: + bad(f"Audit 5D · {A5D_RD} — « Verdict courant » = « {m.group(1)} » MAIS " + f"artefact dit « {a5v_verdict} » → régénérer le README") + # (d) la RÈGLE documentée énumère EXACTEMENT le codomaine du générateur + seg = re.search(r"Verdict global\s*:\s*(.+?)(?: > | ## )", a5v_rd) + if seg is None: + bad(f"Audit 5D · {A5D_RD} — règle « Verdict global : … » INTROUVABLE " + f"(codomaine {sorted(A5V_DOMAIN)} non vérifiable)") + else: + labels = set(re.findall(r"`([A-Z_]+)`", seg.group(1))) + if labels == A5V_DOMAIN: + good(f"Audit 5D · {A5D_RD} — règle « Verdict global » énumère le codomaine " + f"{sorted(A5V_DOMAIN)} == générateur") + else: + miss = sorted(A5V_DOMAIN - labels); extra = sorted(labels - A5V_DOMAIN) + bad(f"Audit 5D · {A5D_RD} — règle « Verdict global » énumère {sorted(labels)} " + f"≠ codomaine générateur {sorted(A5V_DOMAIN)} " + f"(manquants={miss or '—'} en_trop={extra or '—'}) → aligner la règle") + # ============================================================================ # Audit 5D (2e surface du MÊME README) · ÉNUMÉRATION des open items du « Verdict # courant » (control → dimension → propriétaire) — même classe de « vert trompeur ».