From ee421add1d5117f9882da311ca4301ea4d40b896 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 07:43:29 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20Fiscal/ecf=5Fdgii=20:=20le=20JEU=20DE=20DEVISES=20`mon?= =?UTF-8?q?eda`=20(`USD`/`DOP`)=20ANCR=C3=89=20sur=20CLAUDE.md=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le champ que `Encabezado.TipoMoneda` porte dans chaque e-CF DGII — `moneda.options = ["USD","DOP"]` — vit dans `ecf_spec.json` ET, byte-gaté par check_artifacts, dans `out/ecf_plan.json`. C'est une valeur CANONIQUE de CLAUDE.md #10 (« **USD + DOP** devises ») dont le README se réclame « #10 » (README:37/57/77). Or byte-gater une constante prouve la REPRODUCTIBILITÉ, PAS l'ANCRAGE : check_artifacts prouve ecf_plan==build depuis le spec, JAMAIS que spec[moneda]==#10. Les blocs fiscaux amont gatent le FormaPago (Cardnet · #10) + états/champs émetteurs, AVEUGLES au jeu de devises ; le bloc « Fiches #10 » ancre les fiches ERPNext/CRM, AVEUGLE au module fiscal. Piège #6 : RENOMMER/ÉTENDRE les devises dans CLAUDE.md #10 (`USD + DOP`→`USD + EUR`, ou +`EUR`) ET dans le spec de façon cohérente reste byte-VERT tout en faisant émettre à l'e-CF un `TipoMoneda` d'une devise qui CONTREDIT le mandat — « vert trompeur » de la classe de la marque SEO §Entités, la persona Chat OTOIA ou les tokens branding #4, qu'aucune suite tests/ (FONCTIONS de génération, jamais l'ancre à CLAUDE.md) n'attrape. L'agent ERPNext Backend câblerait TipoMoneda hors mandat. Correctif de déclaration : nouveau champ `moneda.options_source` (ecf_spec.json + requis dans ecf.schema.json) citant « CLAUDE.md #10 — devises **USD + DOP** », DISTINCT de `source` (taux TipoCambio, reste null/a_confirmer côté VPS · #8). Artefact régénéré (byte-gate re-verte · 39 tests module verts). Gate ajouté (ci/check_readme_claims.sh, bloc « Fiscal · ecf_dgii — le JEU DE DEVISES `moneda` … ANCRÉ sur CLAUDE.md #10 ») : RÉUTILISE exp["devises"] déjà recomputé depuis #10 (zéro duplication) ; (a) spec[moneda].options == devises #10 ORDRE exact ; (b) artefact byte-gaté idem ; (c) options_source cite CLAUDE.md #10 + chaque devise ; (d) prose README qui se réclame de #10 == jeu exact + toute mention moneda/devise d'une paire concordante (scan ancré sur contexte, évite le faux positif `RNC / ITBIS`). 7 morsures vérifiées (CLAUDE.md #10 rename = 4 bites en cascade · spec options régénéré = le vrai silent green · artefact seul · reorder · options_source sans ancre · README ligne #10 · README table), restauré vert, 7 gates re-verts. ci/README.md (clause récap + paragraphe « 4e surface du MÊME README ») mis à jour. Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-01.md | 65 ++++++++++++ .../fiscal/ecf_dgii/ecf.schema.json | 3 +- .../fiscal/ecf_dgii/ecf_spec.json | 1 + .../fiscal/ecf_dgii/out/ecf_plan.json | 1 + ci/README.md | 31 +++++- ci/check_readme_claims.sh | 99 +++++++++++++++++++ 6 files changed, 198 insertions(+), 2 deletions(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 2721015..6f16077 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -851,3 +851,68 @@ mentions == 54 · ligne source cite roadmap · exit 0. Working tree byte-restaur édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; `eas build`/ `eas submit` App Store #32 / Play Store restent côté agent Mobile · VPS). - **Auto-score 4Big** : 96/100. + +## Session `20260801_073239` · Buffer S8 · Fiscal/ecf_dgii : le JEU DE DEVISES `moneda` (`USD`/`DOP`) ANCRÉ sur CLAUDE.md #10 — le champ que `TipoMoneda` porte dans chaque e-CF DGII, byte-gaté pour la REPRODUCTIBILITÉ mais JAMAIS ancré à la contrainte. Le README s'en réclame « #10 » (README:37/57/77) ; les blocs fiscaux amont gatent le FormaPago (Cardnet · #10) + états/champs émetteurs, AVEUGLES au jeu de devises ; le bloc « Fiches #10 » ancre les fiches ERPNext/CRM, AVEUGLE au module fiscal. + +**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 patron d'ANCRAGE que +les paramètres canoniques `faisabilite/generator` (#9/#10), la marque SEO §Entités +(`Helios RD`), la persona Chat OTOIA (§Architecture cible) ou les tokens branding #4 +— appliqué ici à une valeur ancrée sur **CLAUDE.md #10** (« **USD + DOP** devises »). + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/fiscal/ecf_dgii` — `moneda.options = ["USD","DOP"]` dans le + spec ET (byte-gaté par `check_artifacts`) dans `out/ecf_plan.json`. +- Chaîne de dérivation : CLAUDE.md #10 « **USD + DOP** devises » (INPUT faisant + autorité) → `ecf_spec.json[moneda].options` → `out/ecf_plan.json.moneda.options` + (copie verbatim par `ecflib/builder.py:79`). +- Piège #6 : **byte-gater une constante prouve la REPRODUCTIBILITÉ, PAS l'ANCRAGE**. + `check_artifacts` prouve `ecf_plan==build` depuis le spec, JAMAIS que + `spec[moneda]==#10`. RENOMMER/ÉTENDRE les devises dans CLAUDE.md #10 + (`USD + DOP`→`USD + EUR`, ou +`EUR`) ET dans le spec de façon cohérente reste + byte-VERT tout en faisant émettre à l'e-CF un `TipoMoneda` d'une devise qui + CONTREDIT le mandat — « vert trompeur » qu'aucune suite `tests/` (FONCTIONS de + génération, jamais l'ancre à CLAUDE.md) n'attrape. L'agent ERPNext Backend + câblerait `Encabezado.TipoMoneda` sur une devise hors mandat (le risque même que + la cross-cohérence e-CF veut prévenir). +- **État courant** : aucune dérive — spec/artefact/README concordent avec #10 + (anti-invention #6). Le défaut était la **surface byte-gatée mais non ancrée** + + l'absence de déclaration d'ancrage (`moneda.source` legitimement `null` car il + source le **taux TipoCambio**, à confirmer côté VPS · #8 — l'ancre des OPTIONS + devait vivre dans un champ DISTINCT). + +**Correctif de déclaration** : nouveau champ `moneda.options_source` dans +`ecf_spec.json` (+ requis dans `ecf.schema.json`) citant « CLAUDE.md #10 — devises +**USD + DOP** » ; distinct de `source` (taux, reste `null`/`a_confirmer`). Artefact +`out/ecf_plan.json` régénéré (byte-gate re-verte · 39 tests module verts · le compte +`a_confirmer` inchangé, il ne teste que `moneda.a_confirmer`). + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Fiscal · ecf_dgii — le JEU DE +DEVISES `moneda` … ANCRÉ sur CLAUDE.md #10 », inséré après la famille « Fiches +#10 ») : RÉUTILISE `exp["devises"]` déjà recomputé depuis #10 (zéro duplication), +`want_cur = re.findall("[A-Z]{3}", exp["devises"])`, puis (a) `spec[moneda].options` +== `want_cur` ORDRE EXACT (mord ajout/retrait/réordonnancement) ; (b) l'artefact +byte-gaté idem ; (c) `moneda.options_source` cite `CLAUDE.md #10` + chaque devise ; +(d1) la ligne README « devise (…USD/DOP… · #10) » cite le jeu EXACT ; (d2) toute +mention `moneda`/`devise` d'une paire de devises (scan ancré sur le contexte, évite +le faux positif `RNC / ITBIS`) == le jeu (aucune périmée). Un claim absent échoue +AUSSI (traçabilité). + +**7 morsures vérifiées** : CLAUDE.md #10 `USD+DOP→USD+EUR` (l'ancre est vive → +mord a+b+c+d1 en cascade) · **spec** `options USD/DOP→USD/EUR` régénéré (le vrai +*silent green* → mord a) · **artefact** seul `USD→EUR` (spec+README périmés → mord +b) · **reorder** spec `[DOP,USD]` (ordre exact → mord a) · `options_source` sans +« CLAUDE.md #10 » (ancre déclarée manquante → mord c) · **README** ligne #10 +`USD/DOP→USD/EUR` (prose périmée → mord d1) · **README** table `moneda USD/DOP→ +USD/EUR` (mention périmée → mord d2) ; restauré = vert (a+b+c+d, 3 mentions +README concordantes · exit 0). Working tree byte-restauré (`git checkout --`, +**JAMAIS** `git clean`) · **7 gates re-verts** (validate_json · check_artifacts · +check_readme_claims · check_docs · guard_constraints · check_ci_integrity · +check_regression). + +- `ci/README.md` (clause de récap du pipeline + paragraphe détaillé « jeu de + DEVISES `moneda` … 4e surface du MÊME README, ancré sur #10 ») mis à jour. +- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; + la confirmation du **TipoCambio** USD→DOP + l'émission réelle via Compupar + restent côté agent ERPNext Backend / Compta · VPS). +- **Auto-score 4Big** : 96/100. diff --git a/05_deliverables_mvp/fiscal/ecf_dgii/ecf.schema.json b/05_deliverables_mvp/fiscal/ecf_dgii/ecf.schema.json index 25b2619..ad7a14d 100644 --- a/05_deliverables_mvp/fiscal/ecf_dgii/ecf.schema.json +++ b/05_deliverables_mvp/fiscal/ecf_dgii/ecf.schema.json @@ -150,7 +150,7 @@ }, "moneda": { "type": "object", - "required": ["tipo_moneda_field", "options", "tipo_cambio", "note_cambio", "source", "a_confirmer"], + "required": ["tipo_moneda_field", "options", "options_source", "tipo_cambio", "note_cambio", "source", "a_confirmer"], "additionalProperties": false, "properties": { "tipo_moneda_field": { "type": "string" }, @@ -158,6 +158,7 @@ "type": "array", "items": { "type": "string" } }, + "options_source": { "type": "string" }, "tipo_cambio": { "type": ["number", "string", "null"] }, "note_cambio": { "type": "string" }, "source": { "type": ["string", "null"] }, diff --git a/05_deliverables_mvp/fiscal/ecf_dgii/ecf_spec.json b/05_deliverables_mvp/fiscal/ecf_dgii/ecf_spec.json index e1ec66d..7120d04 100644 --- a/05_deliverables_mvp/fiscal/ecf_dgii/ecf_spec.json +++ b/05_deliverables_mvp/fiscal/ecf_dgii/ecf_spec.json @@ -59,6 +59,7 @@ "moneda": { "tipo_moneda_field": "devise", "options": ["USD", "DOP"], + "options_source": "CLAUDE.md #10 — devises **USD + DOP** (source unique · jamais inventé · #6). Distinct de `source` (taux TipoCambio) qui reste null/a_confirmer.", "tipo_cambio": null, "note_cambio": "Un e-CF libelle en USD exige un TipoCambio (DOP/USD) SOURCE (taux du jour BCRD/DGII) — jamais fabrique (#6). En DOP, TipoCambio non requis.", "source": null, diff --git a/05_deliverables_mvp/fiscal/ecf_dgii/out/ecf_plan.json b/05_deliverables_mvp/fiscal/ecf_dgii/out/ecf_plan.json index c1b33b4..88dc0cb 100644 --- a/05_deliverables_mvp/fiscal/ecf_dgii/out/ecf_plan.json +++ b/05_deliverables_mvp/fiscal/ecf_dgii/out/ecf_plan.json @@ -131,6 +131,7 @@ "USD", "DOP" ], + "options_source": "CLAUDE.md #10 — devises **USD + DOP** (source unique · jamais inventé · #6). Distinct de `source` (taux TipoCambio) qui reste null/a_confirmer.", "tipo_cambio": null, "note_cambio": "Un e-CF libelle en USD exige un TipoCambio (DOP/USD) SOURCE (taux du jour BCRD/DGII) — jamais fabrique (#6). En DOP, TipoCambio non requis.", "source": null, diff --git a/ci/README.md b/ci/README.md index 0f090f6..3797cf4 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 ») **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 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) == 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 ») **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 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) == 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 @@ -713,6 +713,35 @@ contrat déplace le rôle vers `ventes` (portail HORS Compta + prose périmée) diverge du contrat sur le nom (zéro-dup cassé) ; un claim absent échoue AUSSI ; restauré = vert. +Même classe encore sur le **jeu de DEVISES `moneda`** (**4e surface** du MÊME +README `fiscal/ecf_dgii`) — **ancré sur CLAUDE.md #10**. `moneda.options = ["USD", +"DOP"]` — les devises que `TipoMoneda` peut porter dans un e-CF — vit dans le spec ET, +byte-gaté, dans `out/ecf_plan.json` ; c'est une valeur CANONIQUE de #10 (« **USD + +DOP** devises ») dont le README se réclame « #10 » (README:37/57/77). Or **byte-gater +une constante prouve la REPRODUCTIBILITÉ, PAS l'ANCRAGE** : `check_artifacts` prouve +`ecf_plan==build` depuis le spec, jamais que `spec[moneda]==#10`. Les blocs fiscaux +amont gatent le FormaPago (Cardnet · #10) et les états/champs émetteurs, AVEUGLES au +jeu de devises ; le bloc « Fiches #10 » ancre la prose des fiches ERPNext/CRM, AVEUGLE +au module fiscal ; et le `source` du bloc `moneda` est legit. `null` (il source le +**taux TipoCambio**, à confirmer côté VPS · #8), donc l'ancre des OPTIONS vit dans un +champ DISTINCT `options_source`. RENOMMER/ÉTENDRE les devises dans CLAUDE.md #10 +(`USD + DOP` → `USD + EUR`) ET dans le spec de façon cohérente reste byte-VERT tout en +faisant émettre à l'e-CF un `TipoMoneda` d'une devise qui CONTREDIT le mandat — « vert +trompeur » de la classe de la marque SEO §Entités, la persona Chat OTOIA ou les tokens +branding #4, qu'aucune suite `tests/` (FONCTIONS de génération, jamais l'ancre à +CLAUDE.md) n'attrape. On RÉUTILISE `exp["devises"]` déjà recomputé depuis #10 (zéro +duplication) et on exige : (a) `spec[moneda].options` == devises de #10 en ORDRE exact +(mord ajout/retrait/réordonnancement) ; (b) l'artefact byte-gaté idem ; (c) le spec +DÉCLARE l'ancrage — `options_source` cite `CLAUDE.md #10` + chaque devise ; (d) la +ligne README qui se réclame de #10 cite le jeu EXACT + toute mention `moneda`/`devise` +d'une paire de devises == le jeu (aucune périmée). 7 morsures vérifiées : CLAUDE.md #10 +`USD+DOP→USD+EUR` (l'ancre est vive → mord spec+artefact+déclaration+README en cascade) · +spec `options USD/DOP→USD/EUR` régénéré (le vrai *silent green*) · artefact seul +`USD→EUR` (spec+README périmés) · reorder spec `[DOP,USD]` (ordre exact) · `options_source` +sans « CLAUDE.md #10 » (ancre déclarée manquante) · README ligne #10 `USD/DOP→USD/EUR` +(prose périmée) · README table `moneda USD/DOP→USD/EUR` (mention périmée) ; un claim +absent échoue AUSSI ; restauré = vert. + Même classe enfin sur la **cross-cohérence du barème de commissions** (livrable `crm/commissions`) — le CŒUR du module. La section « Cross-cohérence » de son README énumère **à la main** deux ensembles data-derived + une propriété : les états diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 35eac8b..376a528 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -3113,6 +3113,105 @@ if exp is not None: (f" · (pas {exp_excl})" if cites_excl and exp_excl else "") good(f"Fiches #10 · {label} — cite {_cited} == CLAUDE.md #10") +# ============================================================================ +# Fiscal · ecf_dgii — le JEU DE DEVISES `moneda` (`USD`/`DOP`) ANCRÉ sur CLAUDE.md +# #10 — même classe d'ANCRAGE que la CITATION #10 des fiches ci-dessus, mais sur +# une SURFACE distincte : la LISTE d'options de devise de l'e-CF, byte-gatée mais +# JAMAIS ancrée à la contrainte. +# ---------------------------------------------------------------------------- +# `moneda.options = ["USD","DOP"]` — le jeu de devises que `TipoMoneda` peut porter +# dans un e-CF DGII — vit dans le spec ET, byte-gaté par check_artifacts, dans +# out/ecf_plan.json. C'est une valeur CANONIQUE de CLAUDE.md #10 (« **USD + DOP** +# devises ») : le README s'en réclame « #10 » (README:57 « **`devise`** (USD/DOP · +# #10) », README:37/77) MAIS AUCUN gate ne liait cette liste à la contrainte — +# le bloc « Fiscal cross-cohérence » amont gate le FormaPago (Cardnet · #10) et les +# ÉTATS/champs émetteurs, AVEUGLE au jeu de devises ; le bloc « Fiches #10 » ancre +# la prose des fiches ERPNext/CRM, AVEUGLE au module fiscal ; et le `source` du bloc +# moneda est legitimement `null` (il source le TAUX TipoCambio, à confirmer · #8), +# donc l'ancre des OPTIONS vit dans un champ DISTINCT `options_source`. +# Byte-gater une constante canonique prouve la REPRODUCTIBILITÉ, PAS l'ANCRAGE +# (check_artifacts prouve ecf_plan==build depuis le spec, jamais spec[moneda]==#10) : +# renommer/étendre les devises dans CLAUDE.md #10 (`USD + DOP` → `USD + EUR`, ou +# +`EUR`) ET dans le spec de façon cohérente reste byte-VERT tout en faisant émettre +# à l'e-CF un `TipoMoneda` d'une devise qui CONTREDIT le mandat — « vert trompeur » +# de la MÊME classe que la marque SEO §Entités, la persona Chat OTOIA ou les tokens +# branding #4, qu'aucune suite tests/ (FONCTIONS de génération, jamais l'ancre à +# CLAUDE.md) n'attrape. On RÉUTILISE `exp["devises"]` déjà recomputé depuis #10 (zéro +# duplication) et on exige : (a) spec `moneda.options` == devises de #10 (ORDRE exact +# · mord un ajout/retrait/réordonnancement) ; (b) l'artefact byte-gaté idem ; (c) le +# spec DÉCLARE l'ancrage (`options_source` cite « CLAUDE.md #10 » + chaque devise) ; +# (d) la prose README qui se réclame de #10 cite le jeu EXACT + toute mention +# `moneda`/`devise` de paire de devises == le jeu (aucune périmée). Un claim absent +# échoue AUSSI (traçabilité). +# ============================================================================ +if exp is not None: + ECF_MON_RD = f"{D}/fiscal/ecf_dgii/README.md" + want_cur = re.findall(r"[A-Z]{3}", exp["devises"]) # #10 « USD + DOP » → [USD, DOP] + want_set = set(want_cur) + try: + _mon_plan = load("fiscal/ecf_dgii/out/ecf_plan.json")["moneda"] + _mon_spec = json.load(open(f"{D}/fiscal/ecf_dgii/ecf_spec.json", + encoding="utf-8"))["moneda"] + except (OSError, KeyError, ValueError, TypeError) as e: + bad(f"Fiscal devises · moneda spec/artefact illisible/inattendu : {e}") + _mon_plan = _mon_spec = None + if not want_cur: + bad(f"Fiscal devises · CLAUDE.md #10 devises illisibles : {exp['devises']!r}") + elif _mon_plan is not None: + _sp_opt = [str(o).upper() for o in _mon_spec.get("options", [])] + _pl_opt = [str(o).upper() for o in _mon_plan.get("options", [])] + # (a) ANCRAGE — spec moneda.options == devises #10 (ordre exact) + if _sp_opt == want_cur: + good(f"Fiscal devises · (a) spec moneda.options {_sp_opt} == CLAUDE.md #10 " + f"« {exp['devises']} » (ordre exact)") + else: + _miss = sorted(want_set - set(_sp_opt)); _extra = sorted(set(_sp_opt) - want_set) + _d = (f" · absents={_miss}" if _miss else "") + (f" · en trop={_extra}" if _extra else "") + bad(f"Fiscal devises · (a) spec moneda.options {_sp_opt} ≠ CLAUDE.md #10 " + f"{want_cur}{_d} → aligner le spec (ou CLAUDE.md #10)") + # (b) ARTEFACT byte-gaté == devises #10 (ordre exact) + if _pl_opt == want_cur: + good(f"Fiscal devises · (b) ecf_plan.moneda.options {_pl_opt} == CLAUDE.md #10") + else: + bad(f"Fiscal devises · (b) ecf_plan.moneda.options {_pl_opt} ≠ CLAUDE.md #10 " + f"{want_cur} → régénérer l'artefact") + # (c) le spec DÉCLARE l'ancrage : options_source cite CLAUDE.md #10 + chaque devise + _osrc = str(_mon_spec.get("options_source", "")) + _missing_tok = [c for c in want_cur if c not in _osrc] + if "CLAUDE.md #10" not in _osrc or _missing_tok: + bad(f"Fiscal devises · (c) moneda.options_source ne DÉCLARE pas l'ancrage " + f"CLAUDE.md #10 + {want_cur} (manquant : " + f"{'CLAUDE.md #10' if 'CLAUDE.md #10' not in _osrc else _missing_tok}) : {_osrc!r}") + else: + good("Fiscal devises · (c) spec DÉCLARE l'ancrage (options_source cite " + "CLAUDE.md #10 + chaque devise)") + # (d) README — la ligne #10 cite le jeu EXACT + toute paire moneda/devise == le jeu + ecf_mon_rd = read_norm(ECF_MON_RD) + if ecf_mon_rd is not None: + # (d1) la ligne qui se réclame de #10 : « devise (…USD/DOP… · #10) » + m10 = re.search(r"devise[^\n]*?\(([^)]*#10[^)]*)\)", ecf_mon_rd) + if m10 is None: + bad(f"Fiscal devises · (d1) {ECF_MON_RD} — ligne « devise (… · #10) » " + f"INTROUVABLE → la prose ne se réclame plus de #10") + elif set(re.findall(r"[A-Z]{3}", m10.group(1))) != want_set: + bad(f"Fiscal devises · (d1) {ECF_MON_RD} — ligne #10 cite " + f"{sorted(set(re.findall(r'[A-Z]{3}', m10.group(1))))} MAIS #10 dit " + f"{sorted(want_set)} → régénérer le README") + else: + # (d2) AUCUNE mention moneda/devise d'une paire de devises n'est périmée + pairs = re.findall(r"(?:moneda|devise)[^.|\n]{0,40}?\b([A-Z]{3}\s*/\s*[A-Z]{3})\b", + ecf_mon_rd) + stale = [p for p in pairs if set(re.findall(r"[A-Z]{3}", p)) != want_set] + if not pairs: + bad(f"Fiscal devises · (d2) {ECF_MON_RD} — aucune mention moneda/devise " + f"du jeu {sorted(want_set)} → claim absent") + elif stale: + bad(f"Fiscal devises · (d2) {ECF_MON_RD} — mention(s) de devises PÉRIMÉE(S) " + f"{stale} ≠ CLAUDE.md #10 {sorted(want_set)} → régénérer le README") + else: + good(f"Fiscal devises · (d) README cite le jeu #10 {sorted(want_set)} " + f"+ {len(pairs)} mention(s) moneda/devise concordante(s)") + # ============================================================================ # Démo · run-sheet de pitch (2e surface du MÊME README) · la TABLE des SCÉNARIOS # — l'IDENTITÉ de chaque ligne (id · projet+libellé · audience), jamais gatée.