ee421add1d
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) <noreply@anthropic.com>
1103 lines
105 KiB
Markdown
1103 lines
105 KiB
Markdown
# CI/CD · OTO Enterprise OS DTP · Gitea Actions
|
||
|
||
> **Livrable Sprint 1** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` §Sprint 1 · DevOps)
|
||
> _« CI/CD Gitea Actions »_ — gate qualité qui conditionne tous les sprints suivants.
|
||
>
|
||
> **Plateforme : Gitea Actions UNIQUEMENT** (CLAUDE.md contrainte #2 · JAMAIS GitHub).
|
||
|
||
---
|
||
|
||
## 1. Ce que fait le pipeline
|
||
|
||
Workflow : `.gitea/workflows/ci.yml`. Déclenché sur `push` / `pull_request` vers `main`
|
||
et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` par module) :
|
||
|
||
| Job | Script | Rôle | Blocant |
|
||
|---|---|---|---|
|
||
| `constraints-guard` | `ci/guard_constraints.sh` | Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md | ✅ oui |
|
||
| `validate-json` | `ci/validate_json.sh` | Parse strict de tous les `*.json` (schémas Faisabilité↔Publiciste) | ✅ oui |
|
||
| `check-docs` | `ci/check_docs.sh` | Liens Markdown internes + présence auto-score 4Big | ✅ oui (liens) |
|
||
| `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 <N>` == 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
|
||
`bash` + `git` + `python3` (déjà présents sur un runner standard).
|
||
|
||
## 2. Détail des contrôles
|
||
|
||
### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES
|
||
Détecte l'**usage** (pas la simple mention) de :
|
||
- Plateformes git interdites : `github.com`, `gitlab.com`, `bitbucket.org` (#2).
|
||
- CRM interdits : `EspoCRM`, `HubSpot` (#3).
|
||
- Paiement interdit : `Stripe` (#10 · Cardnet only).
|
||
- Écriture directe dans `/var/www/html/static/` (Interdit absolu).
|
||
- Commande `git clean` (Interdit absolu).
|
||
- Remote git pointant ailleurs que Gitea/interne.
|
||
|
||
**Zéro faux positif** : une ligne contenant un marqueur de prohibition
|
||
(`jamais`, `❌`, `only`, `pas de`, `interdit`…) est un rappel de règle → ignorée.
|
||
Escape hatch documenté : ajouter `ci-allow` sur une ligne pour l'exclure.
|
||
|
||
### `validate_json.sh`
|
||
Parse chaque `*.json` suivi (dont `version.schema.json` et
|
||
`projets_master.schema.json`, contrat de données Faisabilité → Publiciste).
|
||
Un JSON cassé casse le pipeline aval → attrapé ici.
|
||
|
||
### `check_docs.sh`
|
||
- **[HARD]** liens Markdown relatifs internes : la cible doit exister.
|
||
- **[SOFT]** livrables `05_deliverables_mvp/*.md` : mention d'auto-score 4Big
|
||
attendue (≥95/100, CLAUDE.md #5). Avertissement seul, non blocant.
|
||
|
||
### `check_artifacts.sh`
|
||
Régénère chaque artefact `05_deliverables_mvp/**/out/` versionné depuis son
|
||
générateur (`build -o <tmp>`) et exige que tout fichier produit soit (a) **suivi
|
||
par git** — jamais un artefact seulement sur disque (`out/` `.gitignore`-é ou
|
||
non-`git add`), qui serait **absent en CI propre** et rendrait le gate « vert en
|
||
local » par artefact fantôme (même classe que le bug `regression_run.json`) — et
|
||
(b) en **égalité byte-for-byte** avec le fichier commité. Découverte automatique
|
||
(zéro liste à la main · #6) : tout module avec un `out/` et un générateur `build`
|
||
entre dans le gate.
|
||
|
||
Cible la **dérive silencieuse** : un module auto-résout des valeurs depuis les
|
||
artefacts d'**autres** modules (ex. `demo/scenarios` lit
|
||
`qa/audit_4big/coverage/ci_modules_count`) ; quand la source grandit, l'artefact
|
||
consommateur devient périmé s'il n'est pas régénéré — dérive qu'aucune suite
|
||
`tests/` (qui teste des fonctions, pas le fichier commité) n'attrape. Correctif :
|
||
`build -o out` puis commit. Les artefacts d'**exécution** non produits par `build`
|
||
(p.ex. `qa/regression/out/regression_run.json`, issu de `run`) sont hors de ce
|
||
gate — ils sont couverts par `check-regression` ci-dessous.
|
||
|
||
### `check_regression.sh`
|
||
Rejoue la **matrice de régression** complète (`qa/regression/regression_gen.py run`,
|
||
~5 s, stdlib pur) vers un tmp et exige que le `regression_run.json` frais soit
|
||
**byte-identique** au commité, puis que son verdict soit `PASS`. Complément direct
|
||
de `check_artifacts` : celui-ci ne rejoue que `build` (→ `regression_plan.json`) et
|
||
laisse hors périmètre l'artefact d'**exécution** `regression_run.json` — pourtant
|
||
c'est *lui* qui porte le compte agrégé (suites · tests · passés · verdict) cité dans
|
||
la doc et les logs — recompté par `check_readme_claims` dans les fiches d'entrée,
|
||
jamais figé en dur ici. Sans ce gate, ce compte pouvait se périmer en
|
||
silence (module + job CI ajoutés sans régénérer la matrice → compte de suites faux ;
|
||
c'est la dérive « demo 18→21 » corrigée à la main), ou une matrice rouge être
|
||
commitée verte. `regression_run.json` ne contient aucun horodatage/hôte → le run est
|
||
déterministe et l'égalité exacte licite. Correctif : `regression_gen.py run -o out`
|
||
puis commit.
|
||
|
||
### `check_ci_integrity.sh`
|
||
Prouve, en lisant `.gitea/workflows/ci.yml`, que le **câblage** du workflow tient —
|
||
car le job `gate` est le **seul verrou de merge** : un check absent de son `needs:`
|
||
ne bloque **rien**, même rouge. Deux invariants :
|
||
- **INV-A** — `gate.needs` == { tous les jobs définis } − { `gate` } − { jobs manuels }
|
||
(un job manuel = gardé par `if: … workflow_dispatch …`, ex. `e2e-baseline`,
|
||
légitimement hors du gate push/PR car il exige un serveur live). Détecte un job
|
||
**oublié** du gate (MISSING → ne bloque pas), une **référence fantôme** (DANGLING →
|
||
typo / job renommé-supprimé) et un job **manuel** glissé dans `needs` (gate en
|
||
attente perpétuelle sur push).
|
||
- **INV-B** — chaque script `ci/*.sh` du dépôt est **câblé** : soit lancé par un job
|
||
(`run: bash ci/<script>`) lui-même dans `gate.needs`, soit **sourcé** par ≥1 gate
|
||
script (lib partagée `ci/lib.sh` — jamais un job propre). Un nouveau gate statique
|
||
**non câblé** (script mort) ou **décâblé**, ou une **lib morte** (sourcée par
|
||
personne), casse le check.
|
||
|
||
Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
||
`qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de
|
||
**module** (ceux portant un `working-directory:`) — les gates **statiques** sans
|
||
working-directory n'étaient gardés par personne. Même classe d'anti-dérive que INV4
|
||
(disque→CI), appliquée au **câblage** CI. stdlib pur (bash/awk/git), zéro réseau.
|
||
|
||
### `check_readme_claims.sh`
|
||
Le `README.md` est le **point d'entrée** du mandat ; sa section « État courant
|
||
(sourcé) » affiche des chiffres et déclare *« Chaque chiffre ci-dessous est sourcé
|
||
vers un artefact commité (anti-invention #6) ; ce README n'introduit aucune donnée
|
||
nouvelle. »* Cette promesse n'était gardée par **aucun** gate : `check_docs.sh` ne
|
||
valide que les **liens** (la cible existe), jamais la **valeur** des nombres. Quand un
|
||
module + son job CI sont ajoutés (21→22 modules, 21→22 suites) ou qu'une promesse
|
||
change de statut (14 `in_repo` + 1 `out_of_scope` → 15 `in_repo`), les chiffres du
|
||
README se **périment en silence** tout en restant « sourcés » vers un artefact qui dit
|
||
autre chose — un README qui se **contredit avec sa propre source** est un « vert
|
||
trompeur » (même classe que la matrice périmée « demo 18→21 » ou INV4, appliqué à la
|
||
doc d'entrée). Ce gate **recompute** chaque chiffre depuis l'artefact cité (jamais une
|
||
liste à la main · #6) et exige l'égalité avec ce qui est **écrit** dans les trois
|
||
docs d'entrée — `README.md` :
|
||
- modules gated `N/M` à `K/100` + verdict `PASS` → `qa/audit_4big/out/quality_report.json` (`totals`) ;
|
||
- `N` suites gated → `qa/regression/out/regression_plan.json` (`totals.suites`) ;
|
||
- `N` promesses (`X` sprint + `Y` métriques), `Z` `in_repo`, verdict `true` → `qa/acceptance/out/acceptance_matrix.json` ;
|
||
- « 13 agents » (×2 : nav + titre) → `git ls-files 03_agents/*/AGENT.md`.
|
||
|
||
Et la fiche QA `03_agents/qa/AGENT.md` (section « Verdict agrégé courant », qui se
|
||
disait « jamais compté à la main » tout en portant un compte figé qui s'est périmé
|
||
— « 21 suites · 534 tests » alors que le run agrégé faisant autorité en disait davantage) :
|
||
- `N` suites · `M` tests · `M` passés · `E` échec · `E` erreur · verdict → `qa/regression/out/regression_run.json` (compte agrégé faisant autorité, commité).
|
||
|
||
Et la fiche ERPNext Backend `03_agents/erpnext_backend/AGENT.md` (ligne « source unique »,
|
||
même compte agrégé jadis saisi à la main qui s'était périmé — « 560 tests ») :
|
||
- `N` tests · `M` suites · verdict → `qa/regression/out/regression_run.json` (matrice de régression du repo, même source faisant autorité que la fiche QA).
|
||
|
||
Et — au grain le **plus fin** — la colonne « Tests » des **tables de livrables** de
|
||
**toutes** les fiches (`03_agents/*/AGENT.md` · crm, faisabilite, publiciste, qa) :
|
||
chaque cellule par suite était saisie à la main et se périmait dès qu'un test était
|
||
ajouté (fiche QA : `acceptance` 31 alors que la suite en portait 37 · `audit_4big`
|
||
35→34 · `regression` 25→26). Le compte **agrégé** ci-dessus (« 564 tests ») ne
|
||
suffit pas : une **compensation** entre deux suites (+1 / −1) laisserait la somme
|
||
juste et les deux lignes fausses. On recompute donc chaque cellule depuis
|
||
`qa/regression/out/regression_plan.json` (`suites[path].test_methods`) — et depuis
|
||
`reglib.discovery.count_tests` (même fonction que le plan · zéro duplication) pour le
|
||
`self_module` `qa/regression`, exclu de la matrice par SoD mais bien documenté.
|
||
|
||
Enfin — même classe, un cran **au-dessus** des cellules — les agrégats rédigés **en
|
||
prose** dans deux fiches (`crm` : « Total CRM **81 tests** (25 + 31 + 25) » ;
|
||
`erpnext_backend` : « **RBAC 60 tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests** »).
|
||
Le **total ET le multiset des composants** (ordre-indépendant) sont recomputés depuis
|
||
`suites[path].test_methods` : une **compensation** entre deux suites laisserait la
|
||
table (gatée ci-dessus) juste et la prose fausse — c'est le même piège que les
|
||
cellules, au niveau agrégé.
|
||
|
||
Même classe encore, hors du domaine « tests » : les comptes **par workspace**
|
||
(cartes / liens / rôles) des portails frontend, transcrits **à la main** dans
|
||
**deux** docs — la table du README du module (`frontend/portails/README.md`) **et**
|
||
la prose de la fiche agent (`03_agents/frontend_console/AGENT.md` : « `OTO Ventes`
|
||
4 cartes/11 liens/12 rôles · … »). Chaque triplet **et** le total « N `Workspace`
|
||
natifs » sont recomputés depuis `frontend/portails/out/MANIFEST.json` (tableau
|
||
`workspaces`, lui-même byte-gaté par `check_artifacts`) : ajouter un rôle/DocType
|
||
au contrat RBAC ferait dériver **les deux** docs en silence sinon.
|
||
|
||
Même classe enfin sur les **comptes RBAC des 3 volets** (`rbac/roleprofile_gen`,
|
||
`rbac/apply_plan`, `rbac/userperm_gen`), recopiés **à la main** dans leurs READMEs
|
||
de module : la table « Nb rôles » **par profil** (+ total bijectif 50), la ligne
|
||
agrégée « Génération réelle : 6 étapes · 50 rôles / 116 DocPerm / 28 UP templates
|
||
/ 6 Role Profile · 4 DocType custom + 5 Company + 4 rôles equipe » (8 comptes de
|
||
tous les volets en une phrase) et la ventilation « 28 `entite` · 16 `groupe` · 2
|
||
`own` · 4 `equipe` ». Chaque nombre est recomputé depuis le `out/MANIFEST.json`
|
||
du volet (recalculé du contrat `rbac_50_roles.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) : ajouter un rôle/DocType ferait dériver **les trois** READMEs
|
||
en silence sinon. **2ᵉ surface `rbac/roleprofile_gen`** : la table « Profils
|
||
générés » ne gatait que le **compte** `Nb rôles` par ligne — sa colonne « Type »
|
||
(`métier`/`technique`) était un **wildcard** (`[^|]*`), donc un portail technique
|
||
classé « métier » (ou l'inverse) passait **vert**. On recompute le classement
|
||
depuis le flag booléen `metier` de `MANIFEST.profiles` et on exige la colonne
|
||
« Type » **exacte** par profil, on gate la prose « **6 profils** (5 métier + 1
|
||
technique) » contre `counts.role_profiles/portails_metier/portails_techniques`, on
|
||
exige que le **nombre de lignes de profil** de la table == `|profiles|` (aucune
|
||
ligne fantôme d'un profil inventé absent du contrat), et en bonus deux
|
||
cohérences croisées qui mordent un MANIFEST **internement** incohérent :
|
||
`portails_metier + portails_techniques == role_profiles` **et** ces comptes
|
||
recomptent les flags `metier` réels des profils. **2ᵉ surface `rbac/apply_plan`** :
|
||
le bloc ci-dessus ne gatait que la ligne **agrégée** de comptes ; la table
|
||
« ## Run-book généré (SPEC §7) » — le **graphe d'application ordonné** que l'agent
|
||
ERPNext suit pas à pas — laissait ses colonnes `#`/`Responsable`/`Dépend de` en
|
||
**wildcard**. Or ces trois colonnes sont data-derived de `out/apply_plan.json`
|
||
(byte-gaté par `check_artifacts`) : le `#` = `order`, `Responsable` =
|
||
`responsable` (`worker`/`vps`/`worker+vps`), `Dépend de` = les n°s d'ordre des
|
||
`depends_on`. Un responsable périmé (étape VPS attribuée au worker) ou une
|
||
dépendance périmée (Role Profile importé **avant** les Role) est un hazard réel
|
||
qu'aucune suite `tests/` (qui teste des fonctions, pas la table commitée) n'attrape
|
||
et que le bloc de comptes ignore. On recompute `#`/`responsable`/`deps` depuis
|
||
`apply_plan.json` et on exige que la table les énonce **exactement** (la colonne
|
||
« Étape » reste libre — paraphrase) ; l'**identité d'ensemble** (lignes de la table
|
||
== étapes de l'artefact) mord toute ligne fantôme ou étape manquante, et trois
|
||
cohérences croisées mordent un plan **internement** incohérent : ordres contigus
|
||
`1..N` sans doublon · `responsable` ∈ {`worker`, `vps`, `worker+vps`} · toute
|
||
dépendance pointe en **arrière** (n° < n° de l'étape). **2ᵉ surface
|
||
`rbac/userperm_gen`** : le bloc des comptes ne gatait que la **ventilation** par
|
||
mécanisme (« 28 `entite` · 16 `groupe` · 2 `own` · 4 `equipe` ») — aveugle à
|
||
**quel** mécanisme applique **quelle** portée. La table « Mapping `scope_donnees`
|
||
→ mécanisme » est la **fonction d'enforcement row-level** (cœur sécurité du
|
||
module) : chaque portée → son `mechanism` Frappe natif + le verdict « Template
|
||
émis ? ». Ces trois sont data-derived de `out/user_permission_plan.json`
|
||
(byte-gaté par `check_artifacts`, chaque entrée portant `scope_donnees` +
|
||
`mechanism` + `user_permission_template`). **Réaffecter** une portée à un
|
||
mécanisme plus permissif (`entite` → `none_consolidated` : le row-level enforcement
|
||
**abandonné**, sur-exposition des données inter-entités), **renommer** un mécanisme
|
||
ou **basculer** le verdict « Template émis ? » laisse la table figée pendant que
|
||
l'artefact dit autre chose — hazard réel qu'aucune suite `tests/` (qui teste des
|
||
fonctions de mapping, pas la table commitée) n'attrape et que le bloc de comptes
|
||
ignore. On recompute par portée le `mechanism` (1er token backtické de la colonne)
|
||
et le verdict « Template émis ? » et on exige la table **exacte** ; l'**identité
|
||
d'ensemble** (portées de la table == portées de l'artefact) mord toute ligne
|
||
fantôme ou portée manquante, et trois cohérences croisées mordent un plan
|
||
**internement** incohérent : chaque portée mappe **un seul** mécanisme (fonction,
|
||
pas relation) · le verdict « Template émis » est **uniforme** sur les entrées
|
||
d'une portée · le template est émis **exactement** pour `user_permission_company`
|
||
(l'invariant « template SSI `entite` »).
|
||
|
||
Même classe enfin côté **SEO** : le total et la ventilation par langue des
|
||
mots-clés (« **258** mots-clés (fr=87 · en=87 · es=84) ») étaient transcrits **à
|
||
la main** dans **deux** docs — la table « Livrables » du README de module
|
||
(`05_deliverables_mvp/seo/README.md`) **et** la même ligne de la fiche agent
|
||
(`03_agents/seo/AGENT.md`) — sans aucun gate. Le quadruplet (total + fr/en/es) est
|
||
recomputé depuis `seo/out/MANIFEST.json` (`counts.keywords_total` +
|
||
`counts.keywords_per_lang`, recomptés de `projets_master.json` à chaque build,
|
||
byte-gaté par `check_artifacts`). Recomputer les **trois** langues, pas seulement
|
||
le total, attrape une **compensation** entre langues (fr −1 / en +1) qui laisserait
|
||
le total juste et la ventilation fausse — ajouter un projet ferait sinon dériver
|
||
**les deux** docs en silence. **2ᵉ surface du même README SEO** : la table
|
||
« Livrables » décrit **en prose** la composition du graphe schema.org (« graphe
|
||
JSON-LD (`Organization` + une `Residence` par projet) »). Le bloc des comptes de
|
||
mots-clés est **aveugle** à ces `@type` : **renommer** le type des listings
|
||
(`Residence` → `Apartment`, un `@type` voisin du rango schema.org), **ajouter** un
|
||
3ᵉ type de nœud (`Offer`/`Product`) ou **casser** la bijection (deux nœuds pour un
|
||
projet) laisse la prose figée pendant que `seo/out/seo_schema_org.json` (byte-gaté
|
||
par `check_artifacts`) dit autre chose. On recompute l'**ensemble** des `@type` du
|
||
`@graph` et on exige que la parenthèse les nomme **exactement** (set-diff : absent
|
||
ET en trop ; plus le type « par projet » nommé == le `@type` des listings). En
|
||
bonus, trois cohérences croisées de l'artefact : le nœud racine `Organization`
|
||
(`@id …#organization`) est un **singleton**, les listings sont **uniformes** (un
|
||
seul `@type`) et en **bijection** avec les projets (`listings == projects ==
|
||
schema_org_nodes−1` du MANIFEST), et le graphe ne porte **aucune** `offers` —
|
||
cohérent avec « aucun prix pour un statut sans prix » (USD · #10). Un claim
|
||
supprimé (« composition INTROUVABLE ») échoue **aussi**.
|
||
|
||
**Identité du nœud racine `Organization` ancrée sur CLAUDE.md §Entités.** Le bloc
|
||
composition ci-dessus gate les `@type` et la bijection, jamais l'**identité de la
|
||
marque** que Google indexe. Or le nœud racine `Organization` de
|
||
`seo/out/seo_schema_org.json` porte `name: "Helios RD"` (la **marque publique** ·
|
||
CLAUDE.md §Entités « **Helios RD** (marque publique) sous WAG ») et `url` == le
|
||
base_url du site. La byte-gate `check_artifacts` prouve seulement que l'artefact
|
||
**se reconstruit** depuis `seo_spec.json` — **pas** que ce nom == la marque nommée
|
||
dans CLAUDE.md. Piège #6 : **renommer** la marque dans
|
||
`seo_spec.json[schema_org.organization.name]` (ou dans CLAUDE.md §Entités) fait
|
||
émettre au JSON-LD **public** un nom qui **contredit le mandat** pendant que la
|
||
byte-gate reste **verte** (l'artefact se reconstruit fidèlement… depuis un spec
|
||
dérivé) — même « vert trompeur » que les tokens branding ancrés sur #4. On
|
||
**re-dérive** la marque de CLAUDE.md §Entités (parenthèse « (marque publique) »,
|
||
zéro duplication), le base_url du MANIFEST byte-gaté et le `country_code` du spec,
|
||
puis on exige : **(a)** `Organization.name` == marque CLAUDE.md ; **(b)**
|
||
`Organization.url` == base_url et `@id` sous base_url ; **(c)** chaque nœud listing
|
||
rattaché à **cette** Organization (`brand.@id` == `org.@id`) et sous base_url ;
|
||
**(d)** `addressCountry` **uniforme** == `country_code` du spec (géo-ciblage
|
||
appliqué à l'identique à chaque nœud) ; **(e)** le spec **déclare** l'ancrage
|
||
(`organization.name` == marque + `source` cite CLAUDE.md). Renommer la marque dans
|
||
CLAUDE.md seul mord **simultanément** l'artefact **et** le spec — l'ancre est vive.
|
||
|
||
**Entités porteuses du DocType CONFOTUR ancrées sur CLAUDE.md §Entités.** Le champ
|
||
Select `entite_porteuse` du DocType `CONFOTUR Application` — le menu déroulant qui
|
||
fixe **quelle entité juridique** porte chaque dossier d'incitation — offre pour
|
||
options les **7 entités canoniques** du mandat (`WAF` · `WA SRL` · `AC Arias
|
||
Cuevas` · `Consortium ECR DR` · `Helios RD` · `Ploutos` · `9060 QC`). Ces options
|
||
sont **dérivées** via `options_source` de `confotur_spec.json[entites]` et
|
||
byte-gatées pour la **reproductibilité** par `check_artifacts` (l'artefact se
|
||
reconstruit depuis le spec) — mais **jamais ancrées** à CLAUDE.md §Entités, leur
|
||
source faisant autorité. Les blocs CONFOTUR amont gatent le **compte** de
|
||
champs/rôles et la cross-cohérence des **permissions**, aveugles à l'identité de
|
||
cette liste. Piège #6 : **renommer** une entité dans CLAUDE.md §Entités
|
||
(`9060 QC`→`9061 QC`) ou dans `confotur_spec.json[entites]` puis régénérer fait
|
||
offrir au DocType un menu qui **contredit/omet** une entité canonique pendant que
|
||
`check_artifacts` reste **vert** (l'artefact se reconstruit… depuis un spec dérivé)
|
||
— même « vert trompeur » que la marque SEO org (§Entités) et les tokens branding
|
||
(#4). On **re-dérive** les 7 entités des tokens en gras de la section « ## Entités »
|
||
de CLAUDE.md, puis on exige : **(a)** options du Select 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"` · le déroulant
|
||
**dérive** de la liste, pas d'un dur) ; **(e)** le README cite l'ancre CLAUDE.md et
|
||
nomme chaque entité (nom complet **ou** 1er token — l'abréviation « AC » de « AC
|
||
Arias Cuevas » est un choix éditorial toléré · #6). Renommer une entité dans
|
||
CLAUDE.md seul mord **simultanément** l'artefact, le spec, sa déclaration **et** le
|
||
README — l'ancre est vive.
|
||
|
||
**3ᵉ surface du même README SEO** : la table « Livrables » décrit **en prose** la
|
||
carte hreflang (« carte `alternate` **FR/EN/ES** + `x-default` par page »). Les
|
||
deux blocs SEO précédents (comptes de mots-clés · graphe schema.org) sont
|
||
**aveugles** à ces codes de langue, qui vivent dans un **troisième** artefact
|
||
(`seo/out/seo_hreflang.json`, byte-gaté par `check_artifacts`) : **ajouter** une
|
||
langue au bundle (`pt` → 4 langues de contenu), **retirer** ou **échanger** `es`
|
||
laisse la prose figée pendant que la carte dit autre chose. On recompute
|
||
l'**ensemble** des langues de contenu (`alternate` hors `x-default`) et on exige
|
||
que la cellule les nomme **exactement** (set-diff : absent ET en trop) + mentionne
|
||
`x-default`. En bonus, quatre cohérences croisées de l'artefact : la carte est
|
||
**uniforme** (toutes les pages portent le même jeu d'`alternate`), `x-default` est
|
||
présent sur **chaque** page, la langue par défaut (`x_default_lang`) ∈ langues de
|
||
contenu (ni vacante ni étrangère), et pour chaque page `x-default`.href ==
|
||
`canonical` == href de la langue par défaut (l'invariant « x-default = langue par
|
||
défaut = canonical » du README). Un claim supprimé (« carte INTROUVABLE ») échoue
|
||
**aussi**.
|
||
|
||
**Catalogue de projets ancré sur CLAUDE.md** (2ᵉ surface du README
|
||
`crm/dossier_vente`) : le champ `Select` `projet` du DocType porteur
|
||
(`out/doctype_oto_dossier_vente.json`, byte-gaté par `check_artifacts`) énumère les
|
||
**9 projets OTO** (`P01 Structure` … `P09 1069 Crisfer`) que le README résume
|
||
« Projet (`Select` **P01..P09** ancré sur CLAUDE.md) ». Ce catalogue est une
|
||
**transcription** : la section « ## Projets » de **CLAUDE.md** est la source
|
||
faisant autorité (#6/#7), mais `check_artifacts` ne prouve que _DocType == spec_
|
||
(byte-for-byte) — la spec recopie elle-même la liste, donc toute la chaîne peut
|
||
**dériver** de CLAUDE.md en restant byte-verte : **renommer** un projet
|
||
(`P03 Nakua` → `P03 Nakua Bay`), en **ajouter** (`P10`) ou en **retirer** un laisse
|
||
le Select périmé tandis que la prose « ancré sur CLAUDE.md » devient fausse. Même
|
||
esprit que le **roadmap-anchor** (INV11 · `roadmap_line` ancré au fichier roadmap) :
|
||
on recompute le catalogue depuis « ## Projets » de CLAUDE.md et on exige que les
|
||
options du Select l'égalent **exactement** (set-diff absent ET en trop, **puis
|
||
ordre**), plus la plage `P01..P<dernier>` + le mot d'ancrage dans le README. En
|
||
bonus, le catalogue CLAUDE.md est vérifié **non vide, sans doublon et contigu**
|
||
depuis `P01` (byte-déterminisme du contrat). Un claim supprimé échoue **aussi**.
|
||
|
||
Même classe enfin sur le **pipeline vente CRM** : le couple « **9 états · 11
|
||
transitions** » (titre de section du README du module `crm/workflow_vente`) et le
|
||
**même** compte d'états « les **9 états** du pipeline » (README du DocType porteur
|
||
`crm/dossier_vente`, dérivé du **même** graphe) étaient transcrits **à la main**
|
||
dans **deux** docs sans aucun gate. Les deux nombres sont recomputés depuis
|
||
`crm/workflow_vente/out/MANIFEST.json` (`counts.states` + `counts.transitions`,
|
||
recomptés du `workflow_vente_spec.json` à chaque build, byte-gaté par
|
||
`check_artifacts`). Recomputer **transitions** en plus de **states** mord une
|
||
dérive du 2ᵉ nombre là où le compte d'états seul (repris dans `dossier_vente`)
|
||
resterait aveugle — ajouter un état/une transition ferait sinon dériver **les
|
||
deux** docs en silence. **2ᵉ surface du même README** : la phrase « **séparation
|
||
des pouvoirs** » énumère **à la main** les transitions quatre-yeux (« confirmer
|
||
réservation, signer contrat, approuver CONFOTUR, annuler/résilier — … `allow_self_approval
|
||
= 0` »). Le bloc de compte est **aveugle** à leur identité : **déclasser** une
|
||
transition sensible (`allow_self_approval` 0→1 dans le contrat) laisse le compte à
|
||
9/11 tandis que l'artefact la retire de l'ensemble ; **promouvoir** une transition
|
||
(ex. `Déposer CONFOTUR` → self=0) l'y ajoute sans que le compte bouge et la prose
|
||
l'**omet** ; un **échange** d'action passe aussi. On recompute l'**ensemble** des
|
||
actions quatre-yeux depuis `crm/workflow_vente/out/workflow.json` (`{action |
|
||
allow_self_approval == 0}`, byte-gaté) et on exige que la phrase l'énumère
|
||
**exactement** (absent **et** en trop mordus par set-diff ; casse ignorée + suffixe
|
||
« (perdu) » élidé — les deux transitions vers l'état *Perdu*, `Annuler`/`Résilier`,
|
||
que la prose groupe « annuler/résilier »). Cohérence croisée : `0 < |quatre-yeux| <
|
||
|transitions|` (l'ensemble sensible est non vide **et** strict). 4 morsures
|
||
vérifiées (déclassement `Signer contrat` capté là où le compte reste 9/11 ·
|
||
promotion `Déposer CONFOTUR` captée · échange prose `signer contrat`→`signer bail`
|
||
capté · énumération supprimée = INTROUVABLE ; restauré = green).
|
||
|
||
**3ᵉ surface du même README** — le CŒUR sécurité du workflow : la table « Rôles =
|
||
contrat RBAC » (l.55-62) nomme, **par pas**, le **rôle RBAC qui garde** chaque
|
||
transition. Les deux blocs ci-dessus gatent le COMPTE et la séparation des pouvoirs
|
||
mais restent **aveugles à l'identité** de ces gardes — data-derived de deux
|
||
artefacts byte-gatés : `workflow.json` (chaque transition porte `allowed`) et
|
||
`MANIFEST.roles_rbac_utilises[].erpnext_role_name` (recomputé du contrat
|
||
`rbac_50_roles.json`). **Réaffecter** un pas monétaire (« Confirmer réservation »
|
||
`Réservations`→`Conseiller` : élévation de privilège), **renommer** un rôle,
|
||
**ajouter** un rôle fantôme ou en **oublier** un laisse la table périmée pendant que
|
||
l'artefact dit autre chose → l'agent ERPNext Backend câblerait le mauvais garde. On
|
||
exige : (1) **identité d'ensemble** — colonne « Rôle » == `{allowed}` de
|
||
`workflow.json` (absent **et** en trop) ; (2) **cohérence artefacts** — `{allowed}`
|
||
== `roles_rbac_utilises` (aucun garde hors manifeste, ni l'inverse) · ensemble non
|
||
vide ; (3) **cross-cohérence par pas** — les gardes des étapes SENSIBLES et uniques
|
||
`Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` (recomputés `allowed`,
|
||
jamais figés) doivent être nommés EXACTEMENT dans leur ligne — ce qui mord la
|
||
**réaffectation vers un rôle déjà présent** que le seul set-diff manquerait. 7
|
||
morsures vérifiées (réaffectation prose `Confirmer réservation` · réaffectation
|
||
artefact `Signer contrat` · rôle fantôme · rôle manquant · incohérence MANIFEST ·
|
||
table supprimée = INTROUVABLE · réaffectation artefact `Approuver CONFOTUR` vers un
|
||
rôle déjà présent — set inchangé, captée par la cross-cohérence par pas ; restauré =
|
||
green).
|
||
|
||
**IDENTITÉ du DocType porteur** — le **NOM** `OTO Dossier Vente` : le module
|
||
`crm/dossier_vente` émet `out/doctype_oto_dossier_vente.json` dont le champ `name`
|
||
**est** le nom du DocType (= `MANIFEST.doctype_name`, byte-gaté par
|
||
`check_artifacts`). Ce nom est le **point d'attache** du pipeline : le Workflow
|
||
vente s'y branche via son `document_type`
|
||
(`crm/workflow_vente/out/workflow.json[0].document_type`). Il est **recopié à la
|
||
main** à plusieurs endroits, dont **aucun** n'était gaté sur le NOM (le bloc
|
||
pipeline ci-dessus ne recompute que le **compte** d'états, aveugle à l'identité) :
|
||
le **titre** du README `dossier_vente` (« # DocType porteur · `OTO Dossier Vente` »)
|
||
et sa ligne « **Nom du DocType** = `document_type` du workflow (`OTO Dossier
|
||
Vente`) », la fiche `03_agents/crm/AGENT.md:25` (« **DocType porteur** `OTO Dossier
|
||
Vente` ») et la fiche `03_agents/onapi_legal/AGENT.md:50` (« cible =
|
||
`workflow.document_type` (`OTO Dossier Vente`) »). Piège : **renommer** le DocType
|
||
dans le spec reconstruit les **deux** artefacts de façon cohérente (le
|
||
`document_type` du workflow suit, les tests — qui testent des FONCTIONS — restent
|
||
verts) tandis que les mentions en prose se **périment en silence** → l'agent
|
||
ERPNext Backend importerait un DocType sous un nom pendant que la fiche/le README
|
||
en nomment un autre, et le Workflow s'attacherait à un DocType **fantôme** — la
|
||
rupture même que « sans lui le pipeline n'a rien à quoi s'attacher » veut
|
||
prévenir. On **recompute** le nom depuis l'artefact (zéro duplication) et on exige
|
||
que chaque prose le nomme **exactement** (backtické). Cohérences croisées :
|
||
`DocType.name == MANIFEST.doctype_name` (cohérence interne du module) **et**
|
||
`== workflow.document_type` (l'attache porteur↔workflow — sinon le Workflow vise un
|
||
DocType fantôme). 7 morsures vérifiées (nom artefact renommé → mord les 6 lignes en
|
||
cascade · `document_type` du workflow divergent → attache rompue · `doctype_name`
|
||
du MANIFEST divergent → incohérence interne · fiche CRM:25 périmée · fiche
|
||
onapi_legal:50 périmée · ligne « Nom du DocType » périmée · titre du README périmé ;
|
||
un claim absent échoue **aussi** ; restauré = green).
|
||
|
||
Même classe encore sur l'**app mobile Expo** : la ligne récap du README du module
|
||
`mobile/app_config` (« **5 onglets** · **44 rôles** couverts · **3 langues** ·
|
||
**13 identifiants `a_confirmer`** ») transcrivait **à la main** un quadruplet dont
|
||
chaque nombre est recompté à chaque build depuis `rbac_50_roles.json` (portails
|
||
métier + rôles), `seo_spec.json` (langues) et la liste `a_confirmer` (credentials
|
||
EAS/stores jamais fabriqués). Les quatre sont recomputés depuis
|
||
`mobile/app_config/out/MANIFEST.json` (`counts.{onglets,roles_couverts,langues,
|
||
identifiants_a_confirmer}`, byte-gaté par `check_artifacts`). Recomputer **les
|
||
quatre** volets mord une dérive de n'importe lequel (ajouter un rôle/portail RBAC,
|
||
une langue, un credential store) là où un seul nombre resterait aveugle. **2ᵉ
|
||
surface du même README** : la même ligne récap **énumère** les 5 onglets
|
||
(« (Ventes/Construction/Achat/Compta/Direction) ») — le patron du compte matche
|
||
la parenthèse par `\([^)]*\)` mais en **ignore le contenu**. Cette liste est
|
||
data-derived : `mobile/app_config/out/role_navigation.json` (byte-gaté) porte un
|
||
onglet par portail métier (bijection 1 onglet ⇔ 1 portail), chaque entrée = son
|
||
`label` « OTO <Nom> ». On recompute la **liste ordonnée** des noms courts et on
|
||
exige que la parenthèse l'énumère **exactement** : un **échange** (Ventes→
|
||
Marketing), un **retrait** (Compta absent — l'énumération tombe à 4 tandis que
|
||
« **5 onglets** » reste juste) ou un **réordre** est mordu là où le seul compte
|
||
est aveugle. Cohérence croisée : `len(role_navigation) == counts.onglets ==
|
||
counts.portails` — mord un compteur du manifeste désynchronisé de sa navigation.
|
||
|
||
Même classe enfin sur le **DocType CONFOTUR Application** (livrable `legal/confotur`) :
|
||
la ligne de synthèse du README du module (« **18 champs** (14 de donnée) · **4
|
||
sections** · **3 rôles** · … · **2 évènements de dépôt** ») **et** la cellule de la
|
||
fiche agent `onapi_legal` transcrivaient **à la main** le même quintuplet, dérivé du
|
||
spec du DocType. Les cinq nombres sont recomputés depuis
|
||
`legal/confotur/out/MANIFEST.json` (`counts.{fields,data_fields,sections,roles,
|
||
depot_events}`, recomptés du `confotur_spec.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) et comparés aux **deux** docs. Recomputer **les cinq** volets mord
|
||
une dérive de n'importe lequel (ajouter un champ/une section/un rôle au spec) là où un
|
||
seul nombre resterait aveugle. **2ᵉ surface du même README CONFOTUR** — le CŒUR
|
||
sécurité du livrable : la section « Cross-cohérence » §Permissions énumère **à la
|
||
main**, PAR rôle, son **portail** ET son **jeu d'actions** RBAC (annoncé « mot pour
|
||
mot les `permissions_cibles` … ni ajout ni retrait ») — `ventes-confotur` (Ventes)
|
||
→ read/write/create/print · `legal-onapi` (Direction) → read/write/create ·
|
||
`legal-directeur` (Direction) → read/write/**submit**/report. Le compte « 3 rôles »
|
||
est **aveugle** à leur identité : **promouvoir** `ventes-confotur` à `submit`
|
||
(élévation de privilège cassant la séparation des pouvoirs dont `is_submittable` est
|
||
déduit), **retirer** une action, **réaffecter** un portail, **renommer** un rôle ou
|
||
**ajouter** une ligne fantôme laisse la prose périmée pendant que les artefacts
|
||
byte-gatés (`out/MANIFEST.json.roles_rbac_utilises[]` + `out/doctype_confotur_
|
||
application.json.permissions[]`) disent autre chose → l'agent ERPNext Backend
|
||
câblerait le mauvais jeu de permissions (le risque même que la cross-cohérence veut
|
||
prévenir), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de mapping RBAC,
|
||
pas la prose) n'attrape. On RECOMPUTE le jeu d'actions + le portail de chaque rôle
|
||
depuis le MANIFEST et on exige que la prose l'encode **exactement** (set-diff :
|
||
absent **et** en trop · casse normalisée), plus l'**identité d'ensemble** (README ⇔
|
||
MANIFEST — aucun rôle fantôme ni manquant). Cohérences croisées en bonus (mordent un
|
||
artefact **internement incohérent**) : MANIFEST ⇄ DocType d'accord sur le jeu
|
||
d'actions par rôle · **séparation des pouvoirs** — `submit` porté par **exactement
|
||
un** rôle (`legal-directeur`, comme le singleton `set_user_permissions` du catalogue
|
||
RBAC) et `is_submittable == True` déduit de sa présence (README:31). 7 morsures
|
||
vérifiées : README promeut `ventes-confotur` à `submit` (en trop) · README réaffecte
|
||
`legal-onapi` Direction→Ventes · README retire `report` de `legal-directeur`
|
||
(sous-ensemble) · README renomme un `role_id` (fantôme + manquant) · énumération
|
||
supprimée (INTROUVABLE) · MANIFEST promeut `ventes-confotur` à `submit` (README
|
||
périmé + séparation cassée + incohérence MANIFEST⇄DocType) · DocType
|
||
`is_submittable=false` alors qu'un rôle porte `submit` (incohérence interne) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur la **synthèse de l'audit 5D** (livrable `qa/audit_5d`) : la
|
||
phrase d'ouverture du README du module (« **17 contrôles** répartis en **5 dimensions
|
||
(5D)** ») **et** la cellule de la fiche agent `qa` (« 17 contrôles / 5 dimensions »)
|
||
transcrivaient **à la main** le même couple, dérivé du catalogue de contrôles. Ce
|
||
couple est recomputé depuis `qa/audit_5d/out/MANIFEST.json` (`controls_total` +
|
||
`len(dimensions)`, byte-gaté par `check_artifacts`) et comparé aux **deux** docs. La
|
||
**ventilation du verdict** portée par le seul README (« **13 PASS · 0 FAIL · 4 à
|
||
confirmer** ») est recomputée à part depuis `qa/audit_5d/out/audit_report.json`
|
||
(`totals.{pass,fail,a_confirmer}`, aussi byte-gaté) : elle mord une **bascule** d'un
|
||
contrôle en A_CONFIRMER (pass 13→12, à confirmer 4→5) là où le total de contrôles
|
||
reste 17 et resterait aveugle. Cohérence croisée en bonus : `controls_total` du
|
||
MANIFEST doit égaler `totals.controls` du rapport.
|
||
|
||
Deuxième surface du **même README** de l'audit 5D : la **table « Verdict courant »**
|
||
énumère **à la main** les 4 open items (`| D1.1 · … | D1 | Direction |` …), chacun
|
||
portant trois faits data-derived — l'`id` du contrôle, sa **dimension** et son
|
||
**propriétaire**. Le bloc de ventilation ci-dessus ne gate que le **compte** (« 4 à
|
||
confirmer ») : un **échange** d'open item (ex. `D2.3 → D3.1`), une **dimension** mal
|
||
étiquetée (`D1.3` rangé sous `D2`) ou un **propriétaire** réattribué laisserait le
|
||
compte à 4 et **passerait en silence**. On RECOMPUTE l'ensemble
|
||
`{control:(dimension, propriétaire)}` depuis `qa/audit_5d/out/audit_report.json`
|
||
(`open_items[]`, byte-gaté) et on exige que la table README l'énumère **exactement** :
|
||
contrôle par **ensemble** (absent **et** en trop mordus, pas seulement présence),
|
||
puis `(dimension, propriétaire)` **par ligne** — même patron que l'énumération des
|
||
confirmations DevOps ou la carte de renormalisation par archétype 4Big. Cohérence
|
||
croisée en bonus : l'ensemble des `control` == `MANIFEST.open_items` (le manifeste qui
|
||
résume le rapport) — mord un manifeste désynchronisé de son propre rapport.
|
||
|
||
Même classe enfin sur le **run-book de déploiement VPS** (livrable
|
||
`devops/deploy_runbook`) : le compte de **phases** (fiche agent `devops` — « graphe
|
||
de **7 phases ordonnées** » **et** « exécute les 7 phases sur le VPS ») **et** le
|
||
compte de **confirmations préalables** (README du module — « renseigner les 7
|
||
confirmations ») transcrivaient **à la main** deux nombres qui, aujourd'hui, valent
|
||
**tous deux 7** — un gate qui n'en recompterait qu'un serait **aveugle** à la dérive
|
||
de l'autre. Chacun est recomputé depuis **son propre** champ de
|
||
`devops/deploy_runbook/out/MANIFEST.json` (`counts.phases` = `len(graph.phase_order)` ·
|
||
`counts.confirmations` = `len(confirmations_ouvertes)`, byte-gaté par
|
||
`check_artifacts`) et comparé aux docs. Bonus anti-dérive : l'**énumération** des
|
||
confirmations du README (les tokens `snake_case`) doit être l'ensemble **exact** de
|
||
`confirmations_ouvertes` — mord l'**échange** d'un nom de confirmation là où le seul
|
||
compte (reste 7) resterait aveugle.
|
||
|
||
**2ᵉ surface `devops/deploy_runbook`** : la **table « Plan de phases généré »** du
|
||
MÊME README (« # | Responsable | Phase | Modules | Dépend de ») porte, par phase,
|
||
quatre données data-derived de `out/deploy_runbook.json` (byte-gaté) — le n° d'ordre
|
||
(`#`), le `Responsable` (worker/vps/worker+vps), l'ensemble des **Modules** gated
|
||
déployés dans la phase, et « Dépend de » (les n°s d'ordre des `depends_on`) — que le
|
||
bloc de comptes ci-dessus ne couvrait pas (colonnes **wildcard**). Défaut réel
|
||
reproduit : la table **omettait** `mobile/app_config` (phase 5) **et** `qa/acceptance`
|
||
(phase 7) — **20 modules listés sur 22** — pendant que le MANIFEST prouvait la
|
||
couverture **bijective 22/22 vs CI** : vert trompeur, l'agent DevOps aurait sauté 2
|
||
modules au portage. Le gate recompute `#`/`Responsable`/`Modules`/`Dépend de` depuis
|
||
`deploy_runbook.json` et exige que la table les énonce **exactement** par phase ;
|
||
identité d'ensemble : {ordres des lignes} == {ordres de l'artefact} (aucune phase
|
||
fantôme/manquante) **et** l'**union** des cellules Modules == les 22 modules de
|
||
l'artefact (aucun module oublié/en trop). La colonne « Phase » reste libre
|
||
(paraphrase). Cohérences croisées en bonus (mordent un plan interne incohérent) :
|
||
ordres contigus `1..N` sans doublon · `responsable` ∈ {worker, vps, worker+vps} ·
|
||
toute dépendance pointe en **arrière** (n° < n° de la phase ⇒ pas de cycle).
|
||
|
||
Même classe enfin sur le **run-sheet de démo** (livrable `demo/scenarios`) : le
|
||
nombre de **modules cités**, transcrit **à la main** dans le diagramme d'architecture
|
||
du README du module (« out/ des modules livrés (**10 modules cités**) »), pointait
|
||
l'artefact généré sans qu'aucun gate ne le recompute. Il est recomputé depuis
|
||
`demo/scenarios/out/MANIFEST.json` (`counts.modules_cites_uniques` = |union des
|
||
`modules_cites` des deux scénarios|, byte-gaté par `check_artifacts`) et comparé au
|
||
README ; ajouter un beat qui cite un module non encore tissé (11), ou retirer la
|
||
dernière citation d'un module (9), ferait dériver le diagramme en silence. Cohérence
|
||
croisée en bonus : `counts.modules_cites_uniques` == |`modules_cites`| (liste
|
||
dédupliquée du manifeste) — mord un compteur désynchronisé de sa propre liste.
|
||
|
||
**2ᵉ surface `demo/scenarios`** : la **table des scénarios** du MÊME README
|
||
(« Scénario | Projet | Audience | Angle ») porte, dans ses trois colonnes
|
||
d'identité, des valeurs data-derived de `out/run_sheet.json` (byte-gaté) —
|
||
l'`id` (`S-P07-BANQUIER`), le couple `projet`+`projet_libelle`
|
||
(`P07 Aqua Terra Las Terrenas`) et l'`audience` (`banquier`) — que le bloc Démo
|
||
ci-dessus ne couvrait pas (il ne gate que le compte de modules). Les libellés
|
||
projets sont d'ailleurs déclarés « proviennent **verbatim** de CLAUDE.md ·
|
||
§Projets » sans aucun ancrage. Le gate (1) **ANCRE** chaque `P{code} {libellé}`
|
||
du run_sheet à l'entrée correspondante de la section « ## Projets » de `CLAUDE.md`
|
||
(verbatim, source unique) ; (2) exige que chaque **ligne** de la table porte
|
||
EXACTEMENT id + `P{code} {libellé}` + audience (fin des colonnes wildcard) ;
|
||
(3) impose l'**identité d'ensemble** — l'ensemble des ids de lignes == ids du
|
||
run_sheet == `counts.scenarios` (aucune ligne fantôme d'un scénario inventé, aucun
|
||
scénario manquant). Cohérences croisées en bonus (mordent un artefact interne
|
||
incohérent) : `run_sheet` ↔ `MANIFEST` d'accord sur `(id, projet, audience)` ·
|
||
`id == S-{projet}-{AUDIENCE}` · `counts.scenarios == |scenarios|` · ids non vides
|
||
et sans doublon. Sans ce gate, renommer un id, classer un scénario sous le mauvais
|
||
projet/audience, ajouter une ligne fantôme ou laisser un libellé projet dériver de
|
||
CLAUDE.md restait « vert » alors que le présentateur pitcherait le mauvais couple
|
||
projet/audience — le risque même que la run-sheet veut éliminer.
|
||
|
||
Même classe enfin sur le **montage Chat OTOIA par portail** (livrable
|
||
`frontend/chat_otoia`) : le nombre de **`Custom Block`**, de **configs runtime** et
|
||
de **portails** couverts, transcrits **à la main** dans DEUX docs (fiche agent
|
||
`frontend_console` — « **N `Custom Block` + M configs runtime** » · README du
|
||
module — « Portails (P, `plateforme` exclu) », « N fixtures `Custom Block` », « M
|
||
configs runtime »), pointaient l'artefact sans qu'aucun gate ne les recompute. Ces
|
||
trois nombres valent **tous 5 aujourd'hui** — un gate qui n'en recompterait qu'un
|
||
serait **aveugle** à la dérive des autres (ajouter un portail au contrat RBAC →
|
||
chat_otoia rebâtit 6 blocks ⁄ 6 mounts ⁄ 6 portails pendant que les docs restent à
|
||
5). Chacun est recomputé depuis **son propre** champ de
|
||
`frontend/chat_otoia/out/MANIFEST.json` (`counts.custom_blocks` · `counts.mounts` ·
|
||
`counts.portails`, byte-gaté par `check_artifacts`) et comparé aux docs. Cohérence
|
||
croisée : `custom_blocks` == `mounts` == `portails` == |`MANIFEST.portails`| (la
|
||
bijection 1 block ⇔ 1 mount ⇔ 1 portail du générateur) — mord un compteur
|
||
désynchronisé de sa propre liste.
|
||
|
||
Même classe enfin sur le **module de recette** (`qa/acceptance`) : le couple
|
||
« **N livrables de sprint** + **M métriques succès MVP** » est transcrit **à la
|
||
main** dans DEUX docs que le bloc racine ne couvre pas — le README du module
|
||
(deux puces) ET la fiche agent QA (« (N livrables + M métriques) »). Le bloc
|
||
racine ne gate que le SEUL README d'entrée (« N promesses (X sprint + Y
|
||
métriques) ») ; ces deux docs de module en dérivent. Ajouter une promesse roadmap
|
||
(l'artefact rebâtit 9/7) ferait dériver les deux docs en silence — piège que ni
|
||
les suites `tests/` ni le **roadmap-anchor** (qui ancre les `roadmap_line` de
|
||
l'ARTEFACT, pas la prose des docs) n'attrapent. Chaque nombre est recomputé depuis
|
||
**son propre** genre de la matrice `qa/acceptance/out/acceptance_matrix.json`
|
||
(`kind` `sprint_deliverable` / `mvp_metric`, byte-gatée par `check_artifacts`) et
|
||
comparé aux deux docs. Cohérence croisée en bonus :
|
||
`MANIFEST.counts.{sprint_deliverables,mvp_metrics}` == le compte de la matrice
|
||
qu'il résume — mord un compteur du manifeste désynchronisé de sa liste.
|
||
|
||
Même classe enfin sur le **barème de l'auditeur 4Big** (livrable `qa/audit_4big`).
|
||
Ironie déjà notée : le module qui NOTE les autres retranscrit son propre référentiel
|
||
de scoring **à la main** dans son README (`05_deliverables_mvp/qa/audit_4big/README.md`)
|
||
— la prose « Note chaque module sur **N critères 4Big déterministes** », la table
|
||
« Critère | Poids | … » (une ligne `id`→poids par critère) et l'archétype
|
||
« `generator` (**N** critères) ». Ces trois surfaces DÉRIVENT de
|
||
`quality_report.json.criteria` (byte-gaté par `check_artifacts`, recomputé du
|
||
référentiel à chaque build) ; ajouter/retirer un critère ou changer un poids ferait
|
||
dériver le README en silence pendant que l'artefact dit autre chose — dérive qu'aucune
|
||
suite `tests/` (qui teste des FONCTIONS, pas la prose) n'attrape. Le bloc racine ne
|
||
gate que la ligne « Résultat courant » (verdict/modules), jamais le barème. On
|
||
recompute donc le **nombre de critères** (prose + « generator »), et **chaque poids**
|
||
depuis `criteria[].weight` (zéro duplication du barème du générateur · #6). Cohérences
|
||
croisées : `Σ poids == 100` (base de renormalisation documentée) ET tout module
|
||
d'archétype `generator` porte EXACTEMENT `len(criteria)` `checks` — c.-à-d.
|
||
« generator = tous les critères », ce qui rend le « generator (N critères) » du README
|
||
vérifiablement vrai (mord une dérive du modèle qui le rendrait faux). Une **compensation**
|
||
(un poids +5 / un autre −5) laisserait `Σ == 100` juste et deux lignes fausses : d'où
|
||
le contrôle **par critère**, pas seulement de la somme. 4 morsures vérifiées : dérive
|
||
prose `5→6` (poids intacts) · dérive poids `TESTS 25→30` (compte intact) · dérive
|
||
`generator (5→4)` · claim prose disparu ; restauré = vert.
|
||
|
||
Une **autre surface** du même README est la carte de **renormalisation par
|
||
archétype** : « `generator` (5 critères) · `data_room` (**sans HANDOFF**) ·
|
||
`parser` (**sans CONTRAT** local ni **HANDOFF**) · `contract` (**sans CLI** ni
|
||
**HANDOFF**) ». Le bloc barème ne recomputait QUE `generator` ; les **exclusions**
|
||
des trois autres archétypes n'étaient recomputées par AUCUN gate. Or elles sont
|
||
DATA-DERIVED : pour un archétype, l'ensemble des critères **applicables** =
|
||
`{check.criterion}` de ses modules dans `quality_report.json` (les non-applicables
|
||
sont **absents** des `checks`) ⇒ **exclus** = ordre du barème − applicables. Changer
|
||
la règle de renormalisation d'un archétype (ex. `data_room` exclut aussi `CLI`)
|
||
ferait dériver cette prose en silence. On recompute donc, PAR archétype, l'ensemble
|
||
des critères exclus et on exige que la parenthèse du README l'énumère **exactement**.
|
||
Bonus d'uniformité : tous les modules d'un même archétype portent le **même** jeu
|
||
applicable (« 1 archétype → 1 jeu de critères », base même de la carte). 3 morsures
|
||
vérifiées : `data_room` « sans HANDOFF »→« sans CLI » (mauvais critère) · `parser`
|
||
exclusion `HANDOFF` retirée (sous-ensemble) · énumération `contract` supprimée ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur le **contrat de format de l'e-NCF** (livrable
|
||
`fiscal/ecf_dgii`) : le format de l'identifiant fiscal électronique DGII
|
||
« **E + tipoeCF(2) + secuencia(10)** » (= **13**) — préfixe, longueur du segment
|
||
tipo, longueur du segment secuencia et longueur totale — est le CŒUR du module (le
|
||
« composeur traçable ») et il est retranscrit **à la main** dans **quatre** endroits
|
||
de prose : le résumé du README (`E + tipoeCF(2) + secuencia(10)`), l'invariant 7 du
|
||
README (`E+tipo(2)+seq(10)=13`, seul à porter la longueur **totale**), la section
|
||
composition du README (`e_ncf = "E" + tipo(2) + secuencia(10)`) et la fiche agent
|
||
`erpnext_backend`. Le bloc racine ne gate que le **compte de tests** e-CF (« 39 ») ;
|
||
ce format n'était recomputé par AUCUN gate. Or il est **data-derived** du bloc
|
||
`e_ncf` d'`fiscal/ecf_dgii/out/ecf_plan.json` (byte-gaté par `check_artifacts`) :
|
||
`prefix` (« E »), `longueur` (13), `regex` (« `^E[0-9]{12}$` ») et `structure`
|
||
(« E + tipoeCF (2 chiffres) + secuencia (10 chiffres) »). Allonger la secuencia (un
|
||
rango DGII à 12 chiffres → `longueur` 15) ou changer le préfixe ferait dériver les
|
||
quatre proses en silence pendant que l'artefact dit autre chose — dérive qu'aucune
|
||
suite `tests/` (qui teste les FONCTIONS `compose_encf`/`parse_encf`, pas la prose)
|
||
n'attrape. On RECOMPUTE `(prefix, tipo_len, seq_len, total)` depuis le SEUL bloc
|
||
`e_ncf` (zéro duplication du format du générateur · #6) et on exige que **chaque**
|
||
prose l'encode exactement. Cohérences croisées en bonus (mordent un artefact
|
||
**internement incohérent**) : `1 + tipo_len + seq_len == longueur` (1+2+10 == 13) ET
|
||
le nombre de chiffres du `regex` == `tipo_len + seq_len` (12 == 2+10). 5 morsures
|
||
vérifiées : drift de l'artefact source (secuencia `10→12`, les 4 proses mordues) ·
|
||
résumé README `tipo 2→3` · invariant README total `13→14` (aveugle au segment) ·
|
||
fiche `erpnext_backend` `secuencia 10→9` · composition README supprimée
|
||
(INTROUVABLE) ; plus la morsure d'incohérence d'artefact (`regex` désaligné du
|
||
segment) ; restauré = vert.
|
||
|
||
Même classe enfin sur la **cross-cohérence e-CF DGII** (2e surface du MÊME README
|
||
`fiscal/ecf_dgii`) — le CŒUR du livrable. Le bloc e-NCF ci-dessus ne gate que le
|
||
**format** de l'identifiant ; la section « Cross-cohérence e-CF ↔ workflow ↔ DocType
|
||
↔ RBAC » énumère **à la main** trois données data-derived d'`out/ecf_plan.json`
|
||
(byte-gaté par `check_artifacts`) : les **états qui émettent** un e-CF « seulement
|
||
réservation et contrat » (README:48) = `{emission_events[].update_value}`, les
|
||
**champs Currency de base** « `montant_reservation`, `montant_contrat` » (README:50)
|
||
= `{emission_events[].base_field}`, et la **FormaPago défaut** « = 3 (Tarjeta) …
|
||
via Cardnet (#10) » (README:59) = `forma_pago_defaut` **ancrée sur CLAUDE.md #10**
|
||
(Cardnet = carte). Aucun gate ne les comparait à l'artefact : AJOUTER un évènement
|
||
(facturer un devis), en RETIRER un, RENOMMER un `base_field` ou CHANGER
|
||
`forma_pago_defaut` (Cardnet abandonné → code 2 Transfert) ferait dériver la prose
|
||
en silence — l'agent ERPNext Backend câblerait le mauvais évènement / la mauvaise
|
||
FormaPago (le risque même que la cross-cohérence veut prévenir), dérive qu'aucune
|
||
suite `tests/` (qui teste les FONCTIONS `ncf`/résolution, pas la prose) n'attrape.
|
||
On RECOMPUTE chaque ensemble/valeur depuis l'artefact (zéro duplication du contrat
|
||
du générateur · #6) et on exige que la prose l'encode exactement (absent ET en trop
|
||
via set-diff · accents/casse normalisés). Cohérences croisées en bonus (mordent un
|
||
plan **internement incohérent**) : `forma_pago_defaut.code` ∈ catalogue
|
||
`formas_pago` · libellé prose ⊆ libellé catalogue du code · ensembles non vides.
|
||
7 morsures vérifiées : prose retire `contrat` des émetteurs · prose renomme
|
||
`montant_contrat` · prose FormaPago `3→2` (drift artefact) · artefact
|
||
`forma_pago_defaut.code 3→2` (prose périmée) · artefact ajoute l'évènement `devis`
|
||
(prose sans `devis` — états ET base_field mordus) · FormaPago prose supprimée
|
||
(INTROUVABLE) · artefact `code→99` hors catalogue (incohérence interne) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur l'**identité du RÔLE émetteur** e-CF (**3e surface** du MÊME
|
||
README `fiscal/ecf_dgii`) — le seul rôle habilité à ÉMETTRE un e-CF. Le bloc e-NCF
|
||
gate le format, le bloc cross-cohérence gate états/champs/FormaPago ; mais aucun ne
|
||
comparait l'IDENTITÉ de ce rôle à ses deux sources. Le bullet `role_id` (README:51-54)
|
||
le NOMME par trois attributs data-derived : son `role_id` « `compta-fiscaliste-ecf` »
|
||
= le rôle que TOUS les `emission_events` d'`out/ecf_plan.json` (byte-gaté) référencent,
|
||
son `erpnext_role_name` « _OTO Compta Fiscaliste eCF_ », et son `portail` « `compta` »
|
||
résolu du contrat `rbac_50_roles.json` (byte-gaté par les 3 MANIFEST). RÉAFFECTER
|
||
l'émission à un autre rôle (dans le plan), le RENOMMER, ou DÉPLACER
|
||
`compta-fiscaliste-ecf` du portail `compta` vers un autre (dans le contrat : l'e-CF
|
||
serait émis HORS Compta — la séparation même que la cross-cohérence pose) ferait
|
||
dériver la prose en silence, dérive qu'aucune suite `tests/` (qui teste les FONCTIONS
|
||
`ncf`/résolution, pas la prose) n'attrape. On RECOMPUTE `role_id`/`erpnext_role_name`
|
||
de l'artefact et `portail`/nom Frappe du contrat (zéro duplication) et on exige la
|
||
prose EXACTE. Cohérences croisées en bonus (mordent un plan/contrat **internement
|
||
incohérent**) : tous les `emission_events` pointent UN SEUL rôle (fonction · non vide) ·
|
||
ce `role_id` existe UNE fois au contrat · le nom résolu dans l'artefact == le contrat
|
||
(zéro-dup) · `portail == compta`. 6 morsures vérifiées : prose renomme le `role_id` ·
|
||
prose renomme le nom Frappe · prose déplace le portail (`compta→ventes`) · artefact
|
||
réaffecte les deux `emission_events` à `ventes-directeur` (5 mords en cascade) ·
|
||
contrat déplace le rôle vers `ventes` (portail HORS Compta + prose périmée) · artefact
|
||
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
|
||
**brouillon exclus** « `lead/visite/devis/abandonné` » (README:47), les **champs de
|
||
base** « `montant_reservation`, `montant_contrat` » (README:50) et l'invariant « on
|
||
ne commissionne QUE des états **soumis** (`doc_status = 1`) » (README:46). Le bloc
|
||
racine ne gate que le **compte de tests** commissions (« 25 », via l'agrégat « Total
|
||
CRM ») ; ces ensembles n'étaient recomputés par AUCUN gate. Or ils sont
|
||
**data-derived** : les brouillons = `{update_value | doc_status == 0}` de
|
||
`crm/workflow_vente/out/workflow.json`, les champs de base = `{base_field}` de
|
||
`crm/commissions/out/commission_plan.json` (les deux byte-gatés par
|
||
`check_artifacts`). PROMOUVOIR un état brouillon en soumis, AJOUTER/RENOMMER un champ
|
||
de base ou ÉCHANGER un état ferait dériver la prose en silence pendant que l'artefact
|
||
dit autre chose — dérive qu'aucune suite `tests/` (qui teste des FONCTIONS
|
||
finance/résolution, pas la prose) n'attrape. On RECOMPUTE chaque ensemble depuis SON
|
||
artefact (zéro duplication du contrat du générateur · #6) et on exige que la prose
|
||
l'énumère **exactement** (absent ET en trop mordus via set-diff · accents/casse
|
||
normalisés). Cohérence croisée en bonus (mord un plan **internement incohérent** avec
|
||
le workflow) : tout `update_value` commissionné ∈ états soumis ET ∉ brouillons —
|
||
l'**invariant 3** du module, non vide. 5 morsures vérifiées : échange brouillon
|
||
(`abandonné→perdu`) · sous-ensemble des champs de base (retrait `montant_contrat`) ·
|
||
énumération des brouillons supprimée (INTROUVABLE) · plan pointant un brouillon
|
||
(`reservation→lead`, invariant 3 cassé) · promotion de l'état `abandonne` en soumis
|
||
dans l'artefact (draft set change, « en trop » dans la prose) ; restauré = vert.
|
||
|
||
Même classe enfin sur le **catalogue RBAC** (livrable `rbac/fixtures_gen`) — deux
|
||
faits data-derived transcrits **à la main** dans le README. (1) La liste des DocTypes
|
||
`custom` à créer avant import « (`Faisabilité`, `Publiciste Log`, `CONFOTUR
|
||
Application`, `API Access`) » (README §« Application sur VPS ») == l'ensemble
|
||
`custom_doctypes_a_confirmer` d'`rbac/fixtures_gen/out/MANIFEST.json` (les DocTypes
|
||
`custom: true` du contrat RBAC). (2) La **séparation des pouvoirs** — « le flag
|
||
`set_user_permissions` n'est émis que pour le rôle **RBAC Admin** » (README:48) ==
|
||
l'ensemble `{role | set_user_permissions == 1}` d'`out/custom_docperm.json`. Le bloc
|
||
racine ne gate que le **compte** (« 50 rôles / 116 DocPerm » via l'agrégat RBAC de
|
||
l'apply_plan) — jamais l'**identité** de ces deux ensembles. AJOUTER un DocType
|
||
`custom` au contrat, RENOMMER/ÉCHANGER l'un des 4 ou PROMOUVOIR un 2ᵉ rôle porteur du
|
||
flag (élévation de privilège) ferait dériver la prose en silence pendant que
|
||
l'artefact byte-gaté dit autre chose — dérive qu'aucune suite `tests/` (qui teste des
|
||
FONCTIONS de mapping, pas la prose du README) n'attrape. On RECOMPUTE chaque ensemble
|
||
depuis SON artefact (zéro duplication du contrat du générateur · #6) et on exige que
|
||
la prose l'énumère/le nomme **exactement** (absent ET en trop mordus via set-diff ·
|
||
accents/casse normalisés). Cohérences croisées en bonus : le catalogue est **non
|
||
vide, sans doublon et trié** (byte-déterminisme du générateur) et la séparation des
|
||
pouvoirs est un **singleton** (ni vide — garde vacante — ni multiple — élévation de
|
||
privilège). 6 morsures vérifiées : échange d'un nom de DocType (`Faisabilité →
|
||
Faisabilite2`) · sous-ensemble (retrait `API Access`) · rôle nommé faux (`RBAC Admin
|
||
→ Ventes Conseiller`) · énumération supprimée (INTROUVABLE) · 2ᵉ rôle promu au flag
|
||
dans l'artefact (singleton cassé) · catalogue MANIFEST non trié (byte-déterminisme) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur une **2ᵉ surface du README des portails** (`frontend/portails`) —
|
||
le §« Hand-off → agent ERPNext Backend » énumère **à la main** les DocTypes `custom` à
|
||
créer avant import « (`CONFOTUR Application`, `Faisabilité`, `Publiciste Log`) », un
|
||
catalogue DATA-DERIVED == `doctypes_custom_a_creer` d'`out/MANIFEST.json` (l'UNION des
|
||
`doctypes_custom` par workspace, eux-mêmes lus du flag `custom` du contrat RBAC pour les
|
||
seuls DocTypes **câblés** dans un portail — c'est donc un ensemble PROPRE au module,
|
||
sous-ensemble de celui de `rbac/fixtures_gen` : `API Access`, custom mais non câblé, en
|
||
est ABSENT). Le bloc portails ci-dessus ne gate que les **comptes** par workspace
|
||
(cartes/liens/rôles) — jamais ce catalogue de hand-off. AJOUTER une carte visant un
|
||
nouveau DocType custom au contrat, en RENOMMER un ou en RETIRER un du câblage laisse la
|
||
consigne périmée (l'agent Backend créerait le mauvais jeu de DocTypes → liens pointant
|
||
dans le vide, le risque même que la consigne prévient) — dérive qu'aucune suite `tests/`
|
||
n'attrape. On RECOMPUTE le catalogue depuis `MANIFEST` et on exige que la prose l'énumère
|
||
**exactement** (set-diff · accents/casse normalisés). Cohérences croisées en bonus :
|
||
catalogue **non vide, sans doublon et trié** (byte-déterminisme) · **égal à l'UNION** des
|
||
`doctypes_custom` par workspace (pas de DocType custom câblé mais oublié du hand-off, ni
|
||
l'inverse). 5 morsures vérifiées : prose renomme `Faisabilité → Faisabilite2` · prose
|
||
retire `Publiciste Log` (sous-ensemble) · énumération supprimée (INTROUVABLE) · MANIFEST
|
||
retire du catalogue un DocType encore câblé (incohérence interne : câblé hors catalogue) ·
|
||
catalogue MANIFEST non trié (byte-déterminisme) ; restauré = vert.
|
||
|
||
Enfin, mêmes racines pour les **paramètres canoniques du générateur de faisabilité**
|
||
(`faisabilite/generator`) : les 6 valeurs imposées à CHAQUE faisabilité — frais d'édition
|
||
`3 %`, marketing `8.5 %`, point d'équilibre `52 %` (CLAUDE.md #9) · devises `USD+DOP`,
|
||
format `Letter US`, paiement `Cardnet` (CLAUDE.md #10) — sont la garantie anti-invention
|
||
(#6) du module. Elles vivent, RECOPIÉES, dans `genlib/model.py::CANONICAL` (le dict émis
|
||
dans chaque rendu — **hors `out/` donc invisible à check_artifacts**) et dans la prose
|
||
README:28, **sans aucun ancrage**. Le seul contrôle existant
|
||
(`test_generator.py::test_canonical_params…`) assert que le RENDU contient « `3 %` »,
|
||
« `52 %` »… mais ces marqueurs sont **hardcodés dans le test** — une copie de plus, jamais
|
||
comparée à CLAUDE.md ; si la contrainte #9 change (`52 %→55 %`), model.py, le test ET le
|
||
README restent `52 %` : tous cohérents entre eux, TOUS contredisant CLAUDE.md → le
|
||
générateur émettrait `52 %` dans chaque faisabilité alors que le mandat dit `55 %`. Comme
|
||
le catalogue projets `dossier_vente` et le roadmap-anchor (INV11), on **ancre** les
|
||
consommateurs à leur source : on RECOMPUTE les 6 valeurs des lignes #9/#10 de `CLAUDE.md`
|
||
(source unique) et on exige qu'elles égalent EXACTEMENT (1) `model.py::CANONICAL` (% par
|
||
valeur numérique · textes accent/espace-insensibles) et (2) l'énumération de la prose
|
||
(set-diff : absent ET en trop). 7 morsures vérifiées : CLAUDE.md #9 `52 %→55 %` (model +
|
||
README périmés) · model.py marketing `8.5 %→9 %` · CLAUDE.md #10 `Cardnet→Azul` (model
|
||
périmé) · README retire `Letter US` (absent) · README ajoute un token `IBAN` (en trop) ·
|
||
énumération README supprimée (INTROUVABLE) · model.py `USD + DOP → USD` ; restauré = vert.
|
||
|
||
Même classe enfin sur la **fiche d'identité du rôle RBAC mobile** — la fiche agent
|
||
`03_agents/mobile/AGENT.md`. Les builds/soumissions stores étant **hors-repo** (#8),
|
||
l'agent mobile le reconnaît et déclare que son **seul ancrage in-repo vérifiable** est
|
||
le rôle `plateforme-mobile` du contrat `rbac_50_roles.json` ; sa ligne « Points de
|
||
contact réellement commités » en recopie donc **à la main** TOUS les attributs
|
||
data-derived : `erpnext_role_name` (« OTO Plateforme Mobile »), `nom_fr`
|
||
(« Développeur Mobile »), `famille`/`portail` (`plateforme`), `entite_principale`
|
||
(`9060 QC`), `niveau` (`2`), `scope_donnees` (`groupe`), `modules` (`Core`+`Website`),
|
||
la **permission** `API Access` custom R/W et la `description`. C'est la carte que
|
||
l'agent ERPNext Backend lit pour **seeder le rôle** (`seed_mobile_rbac.py`). Aucun gate
|
||
ne comparait ces valeurs au contrat : le README `mobile/app_config` n'est gaté que sur
|
||
des **comptes agrégés** (« 5 onglets · 44 rôles »), aveugles à l'identité de CE rôle.
|
||
Élever le `scope_donnees` (`groupe→entite` : sur-portée inter-entités), changer le
|
||
`niveau`/l'`entite_principale`, renommer le rôle, ajouter/retirer un module ou élargir
|
||
la permission (`R/W→R/W/D`) laisserait la fiche périmée en silence pendant que le
|
||
contrat dit autre chose → l'agent câblerait le mauvais rôle (l'invention même que #6
|
||
interdit), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de résolution RBAC,
|
||
pas la prose d'une fiche) n'attrape. On RECOMPUTE chaque attribut depuis le contrat
|
||
(zéro duplication · #6) et on exige la prose EXACTE par attribut ; le sigle de
|
||
permission est **canonique et ordonné** (C/R/W/D/S/X/A) de sorte qu'élargir OU rétrécir
|
||
les verbes change le sigle attendu (un préfixe `R/W` ne satisfait plus `R/W/D`). La
|
||
`description` est recoupée **verbatim**, emphase markdown et point final neutralisés.
|
||
Cohérences croisées : `famille == portail` (bijection portail du contrat), la
|
||
permission est bien `custom`, et le `role_id` figure **une seule fois** dans le contrat.
|
||
12 morsures vérifiées (6 côté contrat, 6 côté fiche) : contrat `scope groupe→entite` ·
|
||
contrat `niveau 2→3` · contrat `+delete` (sigle `R/W→R/W/D`) · contrat description
|
||
réécrite · contrat rôle renommé · fiche `modules Website→Selling` · fiche
|
||
`entité 9060 QC→WAF` · fiche perm `R/W→R/W/D` (sur-revendication) et `R/W→R`
|
||
(sous-revendication) · fiche modules over/under-claim · ligne de rôle supprimée
|
||
(INTROUVABLE) ; restauré = vert.
|
||
|
||
Même classe encore sur les **deux fiches d'identité des rôles RBAC faisabilité** —
|
||
`03_agents/rendu/AGENT.md` (rôle `faisabilite-rendu-3d`) et
|
||
`03_agents/ifc_speckle/AGENT.md` (rôle `faisabilite-ifc-speckle`). Chaque ligne
|
||
« Contact in-repo (vérifié) » recopie **à la main**, du contrat `rbac_50_roles.json`,
|
||
les attributs data-derived RESTITUÉS par la fiche : `erpnext_role_name`, `portail`
|
||
(`construction`), `entite_principale` (`WA SRL`), `niveau` (`2`), plus `nom_fr` &
|
||
`modules` (rendu) et surtout les **deux `permissions_cibles`** — `File` R/W/create +
|
||
`Faisabilité` R (rendu, lecture seule) ou R/W (ifc). C'est la carte que l'agent ERPNext
|
||
Backend lit pour seeder le rôle porteur des rendus / de l'export IFC→GLB. Aucun gate ne
|
||
comparait ces valeurs au contrat : le README `frontend/portails` n'est gaté que sur des
|
||
**comptes de rôles par Workspace**, aveugles à l'identité de CE rôle. Élargir
|
||
`Faisabilité` R→R/W (le rôle Rendu 3D gagnerait l'écriture sur un DocType qu'il ne doit
|
||
que LIRE), changer le `niveau`/l'`entite_principale`, renommer le rôle ou permuter un
|
||
module laisserait la fiche périmée en silence. On RECOMPUTE chaque attribut du contrat
|
||
(zéro duplication · #6) ; les verbes de permission sont rendus en **ordre canonique**
|
||
(read→R · write→W · create/delete/… en toutes lettres) de sorte qu'élargir OU rétrécir
|
||
change la chaîne attendue (`R/W/create` ne satisfait plus `R/W/create/delete`).
|
||
**Différence notable avec la fiche mobile** : la `description` de ces deux fiches est
|
||
une **paraphrase éditoriale** (le contrat porte « (… — CLAUDE.md) » / « les modèles »
|
||
que la fiche condense) — elle n'est donc **pas** gatée verbatim (ne rien réécrire de
|
||
correct pour satisfaire un gate · #6) ; seuls les attributs **structurés**, transcrits à
|
||
l'exact, sont contraints. 11 morsures vérifiées (5 côté contrat, 6 côté fiche) : contrat
|
||
rendu `Faisabilité +write` (sigle `R→R/W`) · contrat rendu `niveau 2→3` · contrat rendu
|
||
`WA SRL→WAF` · contrat rendu rôle renommé · contrat ifc `Faisabilité R/W→R` · fiche
|
||
rendu perm `File R/W/create→…/delete` · fiche ifc perm `R/W→R/W/create` · fiche rendu
|
||
module `Faisabilité→Selling` · fiche ifc `WA SRL→WAF` · fiche rendu `nom_fr` retiré
|
||
(claim absent) · ligne de contact ifc supprimée (INTROUVABLE) ; restauré = vert.
|
||
|
||
Enfin, sur une surface data-derived **DISTINCTE du contrat** que les trois fiches
|
||
précédentes touchent aussi : le **rattachement Workspace** (`Has Role`) de leur rôle —
|
||
QUELLE CONSOLE (Workspace ERPNext natif v15) le rôle peut atteindre. Cette membership ne
|
||
vit PAS dans `rbac_50_roles.json` mais dans `frontend/portails/out/workspace.json`
|
||
(byte-gaté par `check_artifacts`), et chaque fiche l'affirme EN PROSE :
|
||
`03_agents/rendu/AGENT.md:35` — rôle « OTO Faisabilité Rendu 3D » **rattaché au
|
||
Workspace `OTO Construction`** ; `03_agents/ifc_speckle/AGENT.md:35` — rôle « OTO
|
||
Faisabilité IFC Speckle » **rattaché à un Workspace** (générique) ; et l'assertion
|
||
**NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle « OTO Plateforme
|
||
Mobile » **volontairement PAS rattaché au Workspace `OTO Ventes`**. Aucun gate ne les
|
||
couvrait : le bloc « triplets par workspace » gate le **compte** de rôles par Workspace
|
||
(`nb_roles`), aveugle à QUEL rôle ; les blocs Fiches Faisabilité / Fiche Mobile gatent le
|
||
**contrat**, pas la membership `Has Role`. Rattacher le rôle mobile à `OTO Ventes`
|
||
(**sur-exposition console** : le dev mobile gagnerait l'accès au portail Ventes — la
|
||
restriction même que la note #6 pose), retirer le rôle rendu de `OTO Construction`
|
||
(perte d'accès console du spécialiste) ou le **déplacer** vers un autre Workspace
|
||
laisserait la fiche périmée en silence. On RECOMPUTE, depuis `workspace.json`, l'ensemble
|
||
des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat
|
||
par `id`, zéro duplication · #6), et on confronte l'assertion : **in_named** (rendu) —
|
||
membership == {le Workspace nommé} ET la prose le cite ; **in_any** (ifc) — membership
|
||
≥ 1 Workspace ; **not_in_named** (mobile) — rôle ABSENT du `Has Role` du Workspace nommé.
|
||
Un Workspace nommé mais **absent** de l'artefact (typo) échoue avant tout (anti-cible
|
||
fantôme). 9 morsures vérifiées (4 côté artefact, 5 côté fiche/bord) : artefact ajoute le
|
||
rôle mobile à `OTO Ventes` (sur-exposition #6 VIOLÉE) · artefact retire le rôle rendu de
|
||
`OTO Construction` · artefact déplace rendu vers `OTO Achat` · artefact retire ifc de
|
||
tous les Workspaces · fiche rendu renomme le Workspace (`OTO Construction→OTO Ventes`,
|
||
INTROUVABLE) · fiche rendu supprime l'assertion · fiche mobile nomme un Workspace
|
||
fantôme (`OTO Marketing`) · fiche ifc supprime l'assertion générique · fiche mobile passe
|
||
le négatif au positif (retire « volontairement pas ») ; restauré = vert.
|
||
|
||
**Tokens design canoniques `publiciste/branding` ancrés sur CLAUDE.md #4** : la
|
||
contrainte NON-NÉGOCIABLE #4 fixe l'identité visuelle luxury — fond dark `#0a0a12`
|
||
+ accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond**. Ces 4
|
||
tokens sont RECOPIÉS à **quatre** endroits jamais gatés : (a) les CONSTANTES
|
||
`lib/branding.py` (`COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`) — la source
|
||
unique importée par le générateur ET substituée dans le gabarit (`{{COLOR_BG}}…`),
|
||
hors `out/` donc **invisible à `check_artifacts`** ; (b) le **docstring** du même
|
||
fichier ; (c) le README (ligne du gabarit) ; (d) le **tuple d'oracle HARDCODÉ** de
|
||
`tests/test_publiciste.py` (`("#0a0a12","#f0b429","Fraunces","Cormorant Garamond")`).
|
||
Le seul contrôle existant (`test_marque_luxury`) assert que le RENDU contient ces
|
||
marqueurs, mais ils sont une **copie de plus, jamais comparée à CLAUDE.md** : si #4
|
||
change (accent `#f0b429`→`#c9a227`) et qu'on aligne `branding.py`, l'oracle du test
|
||
reste périmé (rouge trompeur) ; si RIEN ne bouge, tout reste vert alors que le site
|
||
public émettrait la mauvaise couleur — invention silencieuse (#6) qu'aucune suite
|
||
`tests/` (qui teste des FONCTIONS, pas l'ancrage à CLAUDE.md) n'attrape. **Même
|
||
patron EXACT** que les paramètres canoniques `faisabilite/generator` (ancrage
|
||
`CANONICAL`↔CLAUDE.md #9/#10). On RECOMPUTE les 4 tokens depuis la ligne #4 de
|
||
CLAUDE.md (zéro duplication) et on exige : (a) `branding.py` CONSTANTES == #4 (hex
|
||
insensible à la casse, fontes à l'exact) ; (b) docstring cite les 4 EXACTS ; (c)
|
||
README cite les 2 fontes ; (d) l'oracle du test == #4 (set-diff). **6 morsures
|
||
vérifiées** : `branding.py` accent `#f0b429→#e0a420` · `branding.py` display
|
||
`Fraunces→Playfair Display` · **CLAUDE.md #4** accent `→#c9a227` (mord SIMULTANÉMENT
|
||
les 3 copies aval : constante + docstring + oracle du test — la valeur de l'ancrage)
|
||
· docstring retire l'accent de la prose · README échange une fonte
|
||
(`Cormorant Garamond→Georgia`) · oracle du test retire `Fraunces` ; restauré = vert.
|
||
|
||
**Colonne « Job CI » des fiches agents == section `jobs:` de ci.yml** : chaque
|
||
table de livrables de `03_agents/*/AGENT.md` porte une colonne **Job CI** qui NOMME,
|
||
entre backticks, le job Gitea Actions exécutant la suite du module
|
||
(`rbac-tests`, `fiscal-ecf-tests`, `frontend-portails-tests`…). Ce nom est **saisi
|
||
à la main** ; la source de vérité est la clé `jobs:` de `.gitea/workflows/ci.yml`.
|
||
Les gates existants ne couvraient PAS ce lien : `check_ci_integrity.sh` prouve que
|
||
chaque job est CÂBLÉ dans `gate.needs` (INV-A/B), et les blocs amont recomputent les
|
||
**comptes** de tests par suite — mais RIEN ne vérifiait que le nom écrit dans la
|
||
fiche DÉSIGNE un job réel. Renommer un job dans ci.yml (`rbac-tests`→`rbac-role-tests`)
|
||
ou mal recopier `demo-scenario-tests` en `demo-scenarios-tests` laisse la fiche
|
||
pointer un job **fantôme** : le CI reste vert (le vrai job tourne sous son nouveau
|
||
nom), la doc d'identité de l'agent ment en silence. On RECOMPUTE l'ensemble des jobs
|
||
depuis la section `jobs:` de ci.yml (la section `on:` — push/pull_request — est AVANT
|
||
et donc exclue ; jamais une liste à la main) et on exige que chaque token `*-tests`
|
||
cité dans une fiche y figure. Direction **fiche→ci.yml** (le consommateur) : ci.yml
|
||
peut définir des jobs non cités (légitime), mais une fiche ne peut pas citer un job
|
||
absent. **33 références vérifiées** ; **2 morsures** : token de fiche
|
||
`seo-tests→seo-suite-tests` (job fantôme) · job ci.yml `rbac-tests` renommé (mord
|
||
SIMULTANÉMENT les 4 fiches qui le citent — erpnext_backend/ifc_speckle/mobile/rendu) ;
|
||
la disparition totale de la colonne échoue AUSSI ; restauré = vert.
|
||
|
||
**Persona & capabilities du Chat OTOIA ancrées sur CLAUDE.md §Architecture cible.**
|
||
Le générateur `frontend/chat_otoia` monte l'assistant conversationnel OTOIA (persona
|
||
**Amélie QC**) dans **chaque portail rôle** ; `out/chat_mount.json` (5 configs runtime)
|
||
et `out/MANIFEST.json` portent la persona `{nom: "Amélie", voix: "multilingual_v2"}`
|
||
et les 4 capabilities `aec.py`+`knowledge.py`+`prompt_engine.py`+`chat.py`. La
|
||
byte-gate `check_artifacts` prouve seulement que ces artefacts **se reconstruisent**
|
||
depuis `chat_spec.json` — **pas** que cette persona/ces capabilities == ce que
|
||
**nomme** CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) » ·
|
||
« OTOIA capabilities : aec.py + knowledge.py + prompt_engine.py + chat.py »). Le bloc
|
||
chat_otoia amont ne gate que la **composition** (portails métier · 1 block⇔1 mount) ;
|
||
le **seul** contrôle d'identité vit dans `tests/` (`test_persona_amelie_sourcee` /
|
||
`test_capabilities_sourcees_sans_ajout`) mais son **oracle est hardcodé** (`"Amélie"`,
|
||
`["aec.py", …]`) — une **copie de plus, jamais comparée à CLAUDE.md**. Piège #6 :
|
||
renommer la persona (`Amélie`→`Sophie`), changer la voix, ou renommer/retirer une
|
||
capability dans `chat_spec.json` (ou dans CLAUDE.md §Architecture cible) laisse le chat
|
||
monté dans **chaque portail** annoncer une persona/voix/capabilities qui **contredit le
|
||
mandat** pendant que la byte-gate **et** les tests restent **verts** (l'artefact se
|
||
reconstruit fidèlement depuis un spec dérivé ; l'oracle du test est une copie) — même
|
||
« vert trompeur » que les tokens branding ancrés sur #4 et la marque SEO §Entités. On
|
||
**re-dérive** persona+capabilities de CLAUDE.md §Architecture cible (source unique) et
|
||
on exige : **(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
|
||
(la notation compacte `aec/knowledge/prompt_engine/chat.py` matchée par radical) ;
|
||
**(e)** l'**oracle du test** == CLAUDE.md (set-diff : absent ET en trop). **8 morsures
|
||
vérifiées** : CLAUDE.md `Amélie→Sophie` (mord simultanément spec/mount/README/test —
|
||
l'ancre est vive) · CLAUDE.md retire `chat.py` · spec `aec.py→exfil.py` (capability
|
||
injectée) · spec `persona.source` sans CLAUDE.md · **un seul** mount à voix altérée
|
||
(byte-gate aveugle) · README retire un radical · oracle du test persona `Amélie→Bob` ·
|
||
oracle du test capabilities réordonné/tronqué ; restauré = vert.
|
||
|
||
**SDK majeur Expo de l'app mobile ancré sur la roadmap Sprint 5 l.56.** La version
|
||
**majeure** du SDK Expo que l'app compagnon `mobile/app_config` cible (`expoSdkMajor`)
|
||
est une donnée **data-derived** : `out/app_config.json` (byte-gaté ⇒ faisant autorité)
|
||
porte `expo.extra.expoSdkMajor = 54`, recopié dans `out/MANIFEST.json`
|
||
(`app.expo_sdk_major`), lui-même dérivé de `mobile_spec.json[app].expo_sdk_major` dont
|
||
la **source faisant autorité** est la roadmap Sprint 5 l.56 « **Rebuild Expo 54** » (le
|
||
spec le **déclare** via `app.expo_sdk_source`). Le README recopie ce « Expo 54 » **cinq
|
||
fois** à la main — dont la ligne de source « Expo SDK **54** | roadmap Sprint 5 l.56 ».
|
||
Le bloc **« Mobile · récap de l'app Expo »** amont ne gate que le quadruplet
|
||
onglets/rôles/langues/identifiants `a_confirmer` (comptes de `MANIFEST.counts`) —
|
||
**aveugle** au majeur du SDK. Aucun gate ne comparait cette valeur à l'artefact ni ne
|
||
l'ancrait à la roadmap : **bumper** le SDK (Expo 55 sort · `eas build` cible 55) dans le
|
||
spec/artefact **sans** toucher au README (ou l'inverse), ou **dériver la roadmap** de
|
||
l'artefact, laisse la prose **périmée en silence** pendant que le graphe byte-gaté dit
|
||
autre chose — l'agent Mobile lancerait `eas build` sur le **mauvais SDK** (le rebuild
|
||
même que la ligne roadmap prescrit) — « vert trompeur » qu'aucune suite `tests/` (qui
|
||
teste des **fonctions** de génération, jamais la prose ni l'ancre à la roadmap)
|
||
n'attrape ; même patron que l'ancrage §Entités/CLAUDE.md (marque SEO) ou les
|
||
`roadmap_line` de la recette. On **re-dérive** le majeur de **son** artefact byte-gaté
|
||
(zéro duplication du contrat #6) et on exige : **(a)** cohérence interne artefact ⇔
|
||
`MANIFEST.app.expo_sdk_major` ⇔ `spec.app.expo_sdk_major` ; **(b)** **ancrage roadmap**
|
||
— la roadmap dit « Rebuild Expo `<N>` » avec `N` == artefact (**ancre vive**) ; **(c)**
|
||
le spec **déclare** son 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` à côté du majeur. **7 morsures
|
||
vérifiées** : README `Expo SDK **54**→**55**` (prose périmée) · **artefact** `54→55`
|
||
(README + roadmap périmés — le vrai *silent green*) · **roadmap** `Rebuild Expo 54→55`
|
||
(artefact périmé · ancre qui dérive) · **MANIFEST** `54→55` (chaîne interne incohérente)
|
||
· spec `expo_sdk_source` sans « roadmap » (ancrage déclaré manquant) · README ligne de
|
||
source sans « roadmap » (ancrage prose manquant) · README retire **toute** mention
|
||
« Expo N » (claim absent) ; restauré = vert · **7 gates re-verts**.
|
||
|
||
Un claim **absent** échoue aussi (la dérive de formulation qui ferait
|
||
disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur
|
||
(bash/git/python3), zéro réseau.
|
||
|
||
### `lib.sh` — helper partagé (sourcé, jamais exécuté seul)
|
||
Les gates commençaient tous par `cd "$(git rev-parse --show-toplevel)"`. **Hors**
|
||
d'un arbre de travail git (tarball, `git archive | tar -x`, `git` absent du PATH),
|
||
`git rev-parse` n'écrit rien → `cd ""` est un **no-op qui RETOURNE SUCCÈS** : le gate
|
||
poursuivait, `git ls-files` renvoyait une liste **vide**, et `validate_json` /
|
||
`guard_constraints` / `check_docs` sortaient **exit 0 VERT en n'ayant RIEN
|
||
contrôlé** — le pire « vert trompeur ». `ci/lib.sh` factorise ce préambule dans
|
||
`cd_repo_root`, qui **échoue bruyamment** (exit `3`, code distinct d'un échec de
|
||
contrôle `1`) hors d'un checkout git. `check_ci_integrity` (INV-B) prouve que
|
||
`lib.sh` est bien sourcé par des gates (pas du code mort) sans exiger qu'il figure
|
||
dans un job.
|
||
|
||
## 3. Exécution locale (avant push)
|
||
|
||
```bash
|
||
bash ci/guard_constraints.sh # contraintes CLAUDE.md
|
||
bash ci/validate_json.sh # schémas JSON
|
||
bash ci/check_docs.sh # liens + score
|
||
bash ci/check_artifacts.sh # reproductibilité out/ (build)
|
||
bash ci/check_regression.sh # fraîcheur matrice régression (run)
|
||
bash ci/check_ci_integrity.sh # intégrité du câblage CI (gate ⊇ tous jobs)
|
||
bash ci/check_readme_claims.sh # chiffres du README == artefacts cités (#6)
|
||
```
|
||
|
||
Chaque script retourne `0` si conforme, `1` sinon. Reproduit exactement ce que
|
||
fait la CI (mêmes scripts, aucune logique cachée côté YAML).
|
||
|
||
## 4. Enregistrement du runner Gitea (ops · à faire sur le VPS)
|
||
|
||
> ⚠ Étape **hors périmètre de ce worker** (touche au VPS). À réaliser par
|
||
> l'agent DevOps. Documenté ici pour traçabilité.
|
||
|
||
Le workflow cible un runner avec le label `ubuntu-latest`. Sur le VPS Gitea
|
||
(`:3015`), enregistrer un `act_runner` :
|
||
|
||
```bash
|
||
# Sur le VPS, récupérer le token runner :
|
||
# Gitea → Site Administration → Actions → Runners → Create new Runner
|
||
act_runner register \
|
||
--instance http://153.75.250.214:3015 \
|
||
--token <RUNNER_TOKEN> \
|
||
--labels ubuntu-latest:docker://node:20-bookworm \
|
||
--name oto-dtp-runner
|
||
act_runner daemon # ou service systemd dédié
|
||
```
|
||
|
||
Vérifier ensuite : Gitea → repo `michel/oto-enterprise-os-dtp` → **Settings →
|
||
Actions** doit être activé, et le runner doit apparaître « Idle ».
|
||
|
||
## 5. Checklist de vérification (DevOps, sur VPS)
|
||
|
||
- `[ ]` Gitea Actions activé au niveau instance ET repo.
|
||
- `[ ]` `act_runner` enregistré, label `ubuntu-latest`, statut Idle.
|
||
- `[ ]` Push de test → les 7 gates statiques apparaissent et passent au vert.
|
||
- `[ ]` PR de test avec violation volontaire → `constraints-guard` bloque (rouge).
|
||
|
||
---
|
||
|
||
**Auto-score 4Big du livrable : 96/100.** _Réserve −4_ : l'enregistrement du
|
||
runner (§4) est hors périmètre repo et reste à confirmer sur le VPS par DevOps ;
|
||
tant que le runner n'est pas Idle, la CI ne s'exécute pas côté serveur bien que
|
||
les scripts soient validés localement.
|