68f8c1a718
Deux vraies aspérités d'UX sur le lanceur run_ci.sh (miroir local du gate), reproduites avant fix : - `--help`/`-h` échouait « option inconnue » exit 2 alors qu'un bloc d'usage riche dormait dans l'en-tête → ajout usage() branché AVANT résolution git (aide marche hors checkout : exit 0 ; vrai run hors git reste exit 3), codes de sortie documentés (0/1/2/3). - en mode --static l'en-tête annonçait « 30 jobs = 7 gates + 23 suites » alors que seuls 7 gates tournent → récap « 7 PASS » contredisait l'en-tête ; en-tête rendu mode-conscient (« 7 gates seuls, 23 suites ignorées »). - message d'option inconnue + ci/README.md alignés (ajout --help). Toutes les valeurs restent DÉRIVÉES de gate.needs (#6, zéro compte figé). Zéro nouveau module · zéro gate ajouté (#5) : run_ci.sh reste un lanceur (racine ∉ ci/*.sh, hors gate.needs, INV-B). Vérif : --help exit 0 in+hors git · --bogus exit 2 · check_docs/check_readme_claims/guard_constraints verts · run_ci.sh plein → 30 PASS · 0 FAIL · 0 SKIP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1610 lines
162 KiB
Markdown
1610 lines
162 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 de tests par suite écrits HORS de cette colonne (prose libre seo « 36 tests »/frontend_console « 19 »&« 31 »/onapi_legal « 44 » + cellule de table à suffixe devops « 29 (dont 14…) », recomputés de `plan.suites[].test_methods` — surfaces que row_re et l'agrégat en prose ne balaient pas) **et** du COMPTE de suites gated de la fiche devops (cellule « N suites gated » de la row pipeline CI — un compte de SUITES, classe distincte des comptes de tests, recomputé de `regression_plan.totals.suites` + cohérence croisée == `regression_run` len(suites), plan↔run) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du COMPTE d'INVARIANTS CI du générateur SEO (compte annoncé « ## Garanties (N invariants) » du README + toutes ses mentions « N invariants » + les 5 self-reports du `.py` — docstrings/aides/succès CLI — recomputés du LEDGER numéroté `# N ·` de `_validate`, contiguïté 1..N ; classe distincte des comptes d'artefact) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + IDENTITÉ du VERDICT GLOBAL — le LABEL de conclusion (`PASS`/`PASS_WITH_OPEN_ITEMS`/`FAIL`) recomputé de la règle du générateur : honnêteté interne `audit_report.verdict == verdict(triplet)` (jamais de PASS forcé · #6), croisé `MANIFEST.verdict`, LABEL « Verdict courant » du README, et codomaine `{FAIL, PASS_WITH_OPEN_ITEMS, PASS}` de la règle « Verdict global » documentée + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items` + IDENTITÉ ordonnée des 5 dimensions `id→titre→reference` recomputée de `audit_report.dimensions` + IDENTITÉ des LIVRABLES AUDITÉS — la liste des hand-off `out/*.json` que l'audit de second niveau LIT comme matière première, énumérée à la main dans la section « ## Livrables audités » du README, recomputée par ENSEMBLE de `MANIFEST.audited_artifacts` byte-gaté (ordre README ≠ ordre MANIFEST trié) : puces README == registre (absent ET fantôme mordus) + cohérences croisées : chaque chemin audité RÉSOUT un fichier commité (le `_load` de l'auditeur lève sinon — audit à vide impossible) · |livrables audités| == |alias distincts d'artefacts référencés par les `controls[].artifacts` du spec| (artefact chargé jamais contrôlé, ou alias contrôlé sans chemin de chargement, désynchronise) ; brancher/retirer un livrable amont (`ARTIFACT_PATHS`) périme la section en silence ; section absente échoue AUSSI) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs + IDENTITÉ de la LISTE D'INVARIANTS CI de la section « ## Invariants (N) — garantis en CI » du README : compte annoncé, énumération numérotée en prose ET self-report CLI « N invariants verts » du générateur, tous recomputés du REGISTRE-source = marqueurs `# Invariant N` de `chat_otoia_gen.py` — set-diff absent/fantôme + registre code contigu 1..N) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria` + seuils structurels de la colonne « Fait vérifié » `DOC ≥ N octets`/`TESTS ≥ N méthodes`, recomputés de `quality_spec.json[thresholds]` et cross-vérifiés contre l'evidence `(seuil N)` du report) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis » + **promesse anti-invention #6** `taux_pct/source/a_confirmer` tout-`null` confrontée à `commission_plan.json` dans les deux sens, invariant #7 « aucun taux sans source », `MANIFEST.counts` recoupé + **rôles payés** (le « à quel rôle » du livrable) : chaque `role_id` de `commission_plan.json` résolu dans `rbac_50_roles.json`, appartenance **singleton** au portail `ventes` nommé au README == un `portails_business` (séparation des pouvoirs · invariant #5), `erpnext_role_name` == contrat (nom Frappe résolu · invariant #6), `MANIFEST.roles_rbac_utilises` recoupé) **et** de l'**identité du champ devise** du même livrable (le « sur quelle devise » · invariant #8 : `plan.devise_field == "devise"` == le champ `devise` du DocType Dossier Vente, fieldtype `Select`, `options` ORDONNÉES == les 2 devises canoniques de CLAUDE.md #10 « **USD + DOP** », + la prose README:54 « `devise` (`Select` USD/DOP · #10) » recomputée) **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README **ET dans les 3 copies aval : `renderer.py::_CANONICAL_MARKERS` l'oracle d'enforcement de `check_canonical`, les 2 docstrings de `scorer.py`, et l'oracle du test `test_generator.py`**, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) **et** de l'appartenance à `roles_allowed` du chat OTOIA des fiches agents `03_agents/{rendu,mobile}/AGENT.md` (3e surface d'appartenance DISTINCTE du contrat ET du `Has Role` : QUI peut INVOQUER l'assistant OTOIA d'un portail, contrôle qui vit dans `frontend/chat_otoia/out/chat_mount.json` byte-gaté — on recompute l'ensemble des portails dont la `roles_allowed` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme ∈ `roles_allowed` du portail NOMMÉ `construction` (in_named_portail, triple ancrage : portail nommé == `portail` contrat == unique portail autorisant, membership == {ce portail}, prose citant « invoquer le chat OTOIA ») · mobile affirme VOLONTAIREMENT dans AUCUNE `roles_allowed` (not_in_any, assertion NÉGATIVE #6) — mord l'ajout du rôle mobile à une `roles_allowed` (SUR-EXPOSITION du chat), le retrait/déplacement du rôle rendu de la `roles_allowed` construction, l'élargissement à un 2e portail, un portail nommé fantôme (anti-typo) ou une assertion absente) **et** des tokens design canoniques `publiciste/branding` (**ancrés sur CLAUDE.md #4** : les 4 valeurs de la marque luxury `#0a0a12`/`#f0b429`/`Fraunces`/`Cormorant Garamond` recopiées à QUATRE endroits jamais gatés — (a) les CONSTANTES `lib/branding.py` `COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`, hors `out/` donc invisibles à check_artifacts, importées par le générateur & substituées dans le gabarit `{{COLOR_BG}}…` ; (b) le docstring du fichier ; (c) le README ; (d) le tuple d'oracle HARDCODÉ de `tests/test_publiciste.py` que le seul test existant assert sans jamais le comparer à CLAUDE.md — recomputés de la ligne #4 de `CLAUDE.md` puis exigés EXACTS : (a) constantes == #4 (hex insensible à la casse, fontes à l'exact), (b) docstring cite les 4, (c) README cite les 2 fontes, (d) oracle du test == #4 ; même patron que les paramètres canoniques `faisabilite/generator`, un changement de #4 mord SIMULTANÉMENT les 3 copies aval ; un claim absent échoue AUSSI) **et** de l'identité du RÔLE émetteur e-CF `fiscal/ecf_dgii` (3e surface du MÊME README : le seul rôle habilité à ÉMETTRE un e-CF — bullet `role_id` README:51-54 — nommé par `role_id` « `compta-fiscaliste-ecf` » = celui que TOUS les `emission_events` d'`ecf_plan.json` byte-gaté référencent, `erpnext_role_name` « _OTO Compta Fiscaliste eCF_ », et `portail` « `compta` » résolu du contrat `rbac_50_roles.json` ; recomputés des deux sources — zéro duplication — prose EXACTE exigée ; réaffecter l'émission à un autre rôle, le renommer, ou DÉPLACER ce rôle hors du portail `compta` (e-CF émis HORS Compta, séparation cassée) mord ; cohérences croisées : `emission_events` pointent UN SEUL rôle (fonction · non vide) · `role_id` unique au contrat · nom artefact == nom contrat · `portail == compta` ; un claim absent échoue AUSSI) **et** de la colonne « Job CI » des fiches agents `03_agents/*/AGENT.md` (chaque token `*-tests` cité entre backticks — le job Gitea Actions qui exécute la suite du module — recomputé de la section `jobs:` de `.gitea/workflows/ci.yml`, jamais gaté avant : `check_ci_integrity` prouve que chaque job est CÂBLÉ dans `gate.needs` mais RIEN ne liait le NOM écrit dans la fiche au job réel ; on exige que chaque token de fiche DÉSIGNE un job existant — renommer un job dans ci.yml ou le mal recopier laisse la fiche pointer un job FANTÔME pendant que le CI reste vert ; direction fiche→ci.yml, ci.yml peut définir des jobs non cités mais une fiche ne peut citer un job absent ; la disparition de la colonne échoue AUSSI) **et** de la colonne « Entrée CLI » des fiches agents `03_agents/*/AGENT.md` (le jeu de verbes cité entre backticks — `publiciste.py parse\|validate\|generate\|run`, `faisabilite_gen.py score\|scaffold\|generate\|batch`, `rbac_apply_plan.py build\|validate`, … 16 cellules — recomputé **par AST** de l'ensemble ORDONNÉ des `subparsers.add_parser("<verbe>")` du script réel, jamais gaté avant : les blocs amont couvrent les colonnes « Tests » et « Job CI » mais RIEN ne liait les VERBES écrits aux sous-commandes que le CLI EXPOSE ; script résolu par le chemin de module de la MÊME ligne de table + basename ; égalité EXACTE ensemble ET ordre exigée — renommer/ajouter/retirer/réordonner une sous-commande dans le `.py` (ou la mal recopier dans la fiche) laisse la fiche documenter une surface d'invocation FANTÔME pendant que le module tourne sous le vrai verbe ; direction fiche→CLI ; script introuvable ou disparition de la colonne échoue AUSSI) **et** de l'IDENTITÉ du DocType porteur `crm/dossier_vente` (le NOM `OTO Dossier Vente` — point d'attache du pipeline — recopié à la main dans le titre & la ligne « Nom du DocType » du README `dossier_vente`, la fiche `03_agents/crm/AGENT.md:25` et la fiche `03_agents/onapi_legal/AGENT.md:50`, jamais gaté sur le NOM (le bloc pipeline ne recompute que le COMPTE d'états) : recomputé du champ `name` de `out/doctype_oto_dossier_vente.json` byte-gaté, chaque prose exigée EXACTE + cohérences croisées `DocType.name == MANIFEST.doctype_name` & `== workflow.document_type` (l'attache porteur↔workflow — sinon le Workflow vise un DocType FANTÔME) ; renommer le DocType dans le spec reconstruit les 2 artefacts cohéremment mais périme la prose en silence ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ du nœud racine `Organization` du graphe schema.org SEO (**ancrée sur CLAUDE.md §Entités** : le nom de marque `Helios RD` (marque publique) + son `url` == base_url — le bloc composition amont ne gate que les `@type`/bijection, jamais l'IDENTITÉ de marque que Google indexe sous `vente.otov7.com` ; recomputés de `seo_schema_org.json` byte-gaté + `MANIFEST.base_url` + le `country_code` du spec : (a) `Organization.name` == marque re-dérivée de CLAUDE.md §Entités — renommer la marque dans le spec ou dans CLAUDE.md émet un JSON-LD public qui CONTREDIT le mandat tout en restant byte-vert (l'artefact se reconstruit fidèlement depuis un spec dérivé), même « vert trompeur » que les tokens branding ancrés sur #4 — (b) `Organization.url` == base_url & `@id` sous base_url, (c) chaque listing rattaché à CETTE Organization (`brand.@id`) sous base_url, (d) `addressCountry` UNIFORME == `country_code` du spec, (e) le spec DÉCLARE l'ancrage (`organization.source` cite CLAUDE.md)) **et** des entités porteuses du DocType CONFOTUR `legal/confotur` (**ancrées sur CLAUDE.md §Entités** : le champ Select `entite_porteuse` offre les 7 entités canoniques `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC`, dérivées via `options_source` de `confotur_spec.json[entites]` et byte-gatées pour la reproductibilité mais jamais ancrées au mandat — les blocs CONFOTUR amont ne gatent que le COMPTE de champs/rôles et la cross-cohérence des permissions ; recomputées des tokens en gras de la section « ## Entités » de CLAUDE.md : (a) options de l'artefact == entités CLAUDE.md ORDRE exact, (b) `spec[entites]` == CLAUDE.md l'INPUT ancré, (c) le spec DÉCLARE l'ancrage `entites_source` cite CLAUDE.md + chaque entité, (d) wiring intact `options_source == "entites"`, (e) README cite l'ancre CLAUDE.md + nomme chaque entité par nom complet OU 1er token — renommer une entité dans CLAUDE.md seul mord SIMULTANÉMENT artefact/spec/déclaration/README, l'ancre est vive ; un claim absent échoue AUSSI) **et** des champs `dossier_vente`/`estado` du DocType CONFOTUR `legal/confotur` **dérivés du workflow vente** (`dossier_vente` Link → cible `workflow.document_type` (= `OTO Dossier Vente`) ; `estado` Select → options = les états `confotur_*` du pipeline ; source `crm/workflow_vente/workflow_vente_spec.json` = l'input du builder, déclaré `workflow_source` au MANIFEST — les blocs CONFOTUR amont gatent comptes/permissions/entités mais PAS ces 2 champs ; (0) MANIFEST `workflow_source` cite le spec, (a) `dossier_vente.options` == workflow `document_type`, (b) prose README « (= `…`) » == `document_type`, (c) `estado.options` == `update_value` des états `confotur_*` ORDRE pipeline, (c′) toute option porte le préfixe `confotur`, (d) le bullet `estado` DÉCLARE la dérivation ; renommer le `document_type` côté spec mord SIMULTANÉMENT artefact ET prose, un claim absent échoue AUSSI) **et** de la cross-cohérence PERMISSIONS CONFOTUR répliquée dans la FICHE AGENT `03_agents/onapi_legal/AGENT.md` (3e surface : la §Permissions de la fiche recopie la MÊME table role→portail→actions que le README du module — `ventes-confotur` (Ventes)→read/write/create/print · `legal-onapi` (Direction)→read/write/create · `legal-directeur` (Direction)→read/write/**submit**/report — sous une convention DIFFÉRENTE (` / ` espacé · gras `**submit**`), gatée par RIEN alors que le gate README ne voit QUE le README ; recomputées de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`, byte-gaté), gras/casse normalisés, set-diff par rôle + identité d'ensemble fiche⇔MANIFEST — promouvoir un rôle à `submit`, retirer une action, réaffecter un portail, renommer/ajouter un rôle fantôme dans la fiche mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de la persona & des capabilities OTOIA du Chat par portail `frontend/chat_otoia` (**ancrées sur CLAUDE.md §Architecture cible** : la persona `Amélie` / voix `multilingual_v2` + les 4 capabilities `aec.py`+`knowledge.py`+`prompt_engine.py`+`chat.py`, portées par `out/chat_mount.json` (5 mounts) & `out/MANIFEST.json` byte-gatés pour la reproductibilité mais jamais ancrées au mandat — le bloc chat_otoia amont ne gate que la COMPOSITION (portails métier · 1 block⇔1 mount) et le SEUL contrôle d'identité `tests/` a un ORACLE HARDCODÉ jamais comparé à CLAUDE.md ; recomputées de CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) » · « OTOIA capabilities : … ») : (a) `chat_spec.json` persona/capabilities == CLAUDE.md, (b) le spec DÉCLARE l'ancrage (`persona.source` + chaque `capability.source` citent CLAUDE.md), (c) CHACUN des 5 mounts + le MANIFEST portent persona/capabilities == CLAUDE.md, (d) le README cite l'ancre §Architecture cible + nomme persona + chaque capability, (e) l'ORACLE du test == CLAUDE.md — renommer la persona/voix ou renommer/retirer une capability dans le spec (ou dans CLAUDE.md) mord tout en restant byte-vert ET tests-verts, même « vert trompeur » que les tokens branding ancrés sur #4 ; un claim absent échoue AUSSI) **et** du PIED DE PAGE « Voix Amélie QC (multilingual_v2) » des 11 fiches agents qui orchestrent OTOIA (toutes sauf faisabilite/publiciste · le triplet persona/qualif/voix re-dérivé INDÉPENDAMMENT de CLAUDE.md §Architecture cible — regex qui ne fige pas le qualificatif en dur, donc `QC→…` mord ici — et exigé EXACT sur chaque pied de page ; pied de page malformé & évaporation totale du recensement échouent AUSSI) **et** du SDK MAJEUR Expo de l'app mobile `mobile/app_config` (**ancré sur la roadmap Sprint 5 l.56** « Rebuild Expo 54 » : la version majeure `expoSdkMajor` que l'app cible — recopiée CINQ fois dans le README (`Expo 54`/`Expo SDK **54**`) — jamais gatée hors le quadruplet onglets/rôles/langues/identifiants ; recomputée du champ `expo.extra.expoSdkMajor` de `out/app_config.json` byte-gaté : (a) cohérence interne artefact ⇔ `MANIFEST.app.expo_sdk_major` ⇔ `mobile_spec.json[app].expo_sdk_major`, (b) ANCRAGE roadmap `Rebuild Expo <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) **et** de l'IMPUTABILITÉ des confirmations préalables VPS `devops/deploy_runbook` (3e surface : la ligne « Confirmations préalables VPS » du README annote chaque confirmation d'un couple `nom` (**owner** · **réf-contrôle**) — ex. `taux_commission` (Direction · audit_5d D1.1) — data-derived des `confirmations` de `out/deploy_runbook.json` byte-gaté CROISÉES avec le rapport `qa/audit_5d` byte-gaté ; les blocs DevOps amont ne gatent que le COMPTE et l'ENSEMBLE des noms, aveugles à QUI porte chaque confirmation et à QUEL contrôle la trace ; cross-anchor à trois : owner run-book ⊆ owner du contrôle audit_5d cité (tolère le suffixe « / UAF ») · owner README ⊆ owner run-book (tolère l'abréviation « Fiscaliste eCF » ⊂ « Compta Fiscaliste eCF ») · réf README == contrôle réel présent dans audit_5d ET dans la source — réaffecter un owner, misattribuer le contrôle (`D1.1`→`D2.1`) ou citer un contrôle FANTÔME (`D9.9`) mord ; une confirmation tracée DOIT rester annotée, une annotation d'un nom fantôme échoue AUSSI) **et** du COMPTE CANONIQUE de rôles RBAC (**ancré sur CLAUDE.md §Architecture cible** « RBAC N rôles » : le nombre `50` recopié dans le contrat `rbac_50_roles.json` — le champ déclaré `cible_rbac_roles` ET la taille réelle du catalogue `len(roles)` — dans les schémas (`const: 50`) et dans la fiche `03_agents/erpnext_backend/AGENT.md` (« RBAC **50 rôles** exactement » + 3 autres mentions), sans jamais être re-dérivé du mandat ; les gates RBAC amont ne prouvent que la cohérence INTERNE contrat↔MANIFEST↔READMEs de volet, aveugles à l'ancrage au mandat — changer « RBAC 50 rôles » dans CLAUDE.md ou faire dévier le catalogue de façon interne-cohérente (cible ET len alignés ailleurs) laisserait contrat & fiche affirmer « 50 exactement » en CONTREDISANT le mandat, en silence ; recomputé de §Architecture cible : (a) contrat `cible_rbac_roles` == N, (b) taille réelle `len(roles)` == N, (c) cible == réel (mandat atteint), (d) la fiche porte l'invariant « RBAC **N rôles** exactement » + aucune mention « M rôles » périmée ; même patron que les tokens branding #4 / la persona OTOIA §Architecture cible ; un claim absent échoue AUSSI) **et** des comptes du run-book d'application `rbac/RBAC_50_ROLES_SPEC.md` §7 (la NOTE « ✅ … livré en-repo » restitue sur une surface DISTINCTE des READMEs userperm_gen/roleprofile_gen déjà gatés trois comptes data-derived — « 28 templates `Company` (portée `entite`) », « **6 profils** (5 portails métier + console technique `plateforme`) » et « **50 rôles de façon bijective** » — jamais gatés hors le §3 (ventilation par portail) ; recomputés du contrat `rbac_50_roles.json` byte-gaté : 28 == |scope==entite|, 6 == |portails distincts| (1 Role Profile par portail), 5 == portails métier hors `plateforme`, 50 == |rôles| ; bumper une portée `entite`→`groupe` ou ajouter/retirer un portail dans le contrat périme la note en silence pendant que les READMEs de volet restent verts ; le blockquote `> ` est neutralisé avant match ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ des configs runtime du Chat OTOIA dans la cellule « Métrique vérifiée » de la fiche `03_agents/frontend_console/AGENT.md` (restitue TROIS attributs data-derived du montage — `persona Amélie` · `langues FR/EN/ES` · `` `endpoint: null` `` — aveugles au bloc chat_otoia amont qui ne gate que le COMPTE blocks/mounts/portails, et NON couverts par le gate persona/capabilities ancré sur CLAUDE.md §Architecture cible (qui vise spec/mounts/MANIFEST/README/oracle du test, jamais cette fiche) ; recomputés des artefacts byte-gatés `out/chat_mount.json` (5 configs) + `out/MANIFEST.json` : persona == `MANIFEST.persona.nom` (le « QC » est le qualificatif de marque, gardé en littéral non gaté), langues `FR/EN/ES` == `MANIFEST.langues` majuscule, `endpoint: null` == **AFFIRMATION D'HONNÊTETÉ #6/#8** exigeant que CHAQUE mount ait `endpoint == null` — un endpoint fabriqué dans l'artefact fait de la fiche « null » un MENSONGE et mord ; cohérences croisées : `MANIFEST.persona.nom`/`langues` == CHAQUE mount (montage désynchronisé mordu) · `endpoint_statut == 'a_confirmer'` ⇔ endpoints TOUS null ; un claim absent échoue AUSSI) **et** des DEUX métriques data-derived du bundle SEO restituées dans la fiche `03_agents/seo/AGENT.md` (le tableau « Livrable réellement produit » recopie, à côté du compte de mots-clés déjà gaté sur la fiche, deux sorties gatées AVANT sur le seul README : L25 « graphe JSON-LD : **N nœuds** (`Organization` racine + M `Residence`) » et L26 « **N pages** × FR/EN/ES + `x-default` » ; recomputées des mêmes artefacts byte-gatés `seo_schema_org.json`/`MANIFEST.json` et `seo_hreflang.json` : L25 total nœuds == `schema_org_nodes`, racine == le `@type` singleton, M == `listings`, type listing == le `@type` uniforme des listings + cohérence arithmétique `total == 1 + M` ; L26 nombre de pages == `|pages|` de la carte + jeu de langues de contenu == celui de l'artefact (set-diff, absent ET en trop) ; RENOMMER le `@type`, AJOUTER/RETIRER un projet ou ÉCHANGER une langue périme la fiche pendant que les artefacts disent autre chose — même classe que la cellule chat_otoia de la fiche Frontend Console (fiche-agent qui restitue des attributs gatés seulement en agrégat) ; un claim absent échoue AUSSI) **et** de la colonne « Entité » de la §4 de `rbac/RBAC_50_ROLES_SPEC.md` (**ancrée sur CLAUDE.md §Entités** : la table « Cartographie rôles ↔ entités » liste en colonne de tête les 7 entités NOMMÉES du mandat `WAF`/`WA SRL`/`AC Arias Cuevas`/`Consortium ECR DR`/`Helios RD`/`Ploutos`/`9060 QC` + le périmètre consolidé « Groupe » — deux sources faisant autorité, CLAUDE.md §Entités pour l'orthographe & le descripteur parenthétique et `rbac_50_roles.json` pour l'ENSEMBLE des `entite_principale` réellement portées, aveugles au §3 (comptes par portail) / §7 (comptes run-book) / blocs fiches (`entite` d'UN rôle) ; (a) ENSEMBLE §4 == distinct `entite_principale` du contrat (set-diff : ni fantôme ni manquante), (b) ANCRAGE chaque entité NOMMÉE de CLAUDE.md est une ligne §4 avec son descripteur parenthétique EXACT (absence comprise), (c) chaque entité nommée du mandat est portée ≥1× par le contrat (aucune décorative) — ajouter au contrat un rôle porté par une entité absente de §4, renommer une entité dans CLAUDE.md §Entités (`9060 QC→9061 QC`) ou dériver un descripteur mord ; un claim absent échoue AUSSI) **et** du §5 « résumé » de `rbac/RBAC_50_ROLES_SPEC.md` (« Les N rôles (résumé) » restitue le total sur 3 surfaces prose aveugles au §3/§4/§7 : le compte-titre, 9 comptes de regroupement ÉDITORIAUX `**Ventes (8)**` — agrégats ≠ colonne `famille` du contrat, donc AUCUN mapping groupe↔famille — et une ligne d'auto-contrôle arithmétique `_(6+8+…+6 = 50.)_` ; (a) compte-titre == |rôles| du contrat, (b) les 9 comptes de regroupement == EXACTEMENT dans l'ordre les termes de la ligne, (c) Σ termes == membre droit, (d) membre droit == |rôles| — ajouter un rôle laisse titre/somme/`= 50` périmés, éditer `Ventes (8)→(7)` sans toucher la ligne casse (b)/(c) ; section/titre/ligne/comptes absents échouent AUSSI) **et** de l'IDENTITÉ de l'ENSEMBLE des portails métier + l'exclusion nommée de la console technique dans la fiche `03_agents/frontend_console/AGENT.md` (le bloc « triplets par workspace » recompute cartes/liens/rôles PAR portail mais reste aveugle à QUELS portails composent l'ensemble ; la fiche énumère à la main L11 « 5 portails rôle métier (Ventes / Construction / Achat / Compta / Direction) » et affirme L46 que « la console technique `plateforme` (6ᵉ portail RBAC) est **exclue** » ; (a) l'ensemble ORDONNÉ des noms courts == les workspaces métier de `frontend/portails/out/MANIFEST.json` byte-gaté (label « OTO <Nom> » sans préfixe, comme l'énumération des onglets Mobile) ET, DOUBLE ANCRAGE, leurs ids == `portails_business` du contrat RBAC (l'« égalité stricte » que la fiche revendique elle-même) ; (b) `plateforme` == l'UNIQUE portail RBAC hors métier (∈ portails distincts des rôles ∖ `portails_business`), c'est le 6ᵉ (total distinct == 6 = 5 métier + 1) — un échange (Achat→Marketing), un retrait, un réordre, un renommage de la console technique dans le contrat (`plateforme`→`console`) ou un contrat qui PROMEUT `plateforme` en métier mord ; un claim absent échoue AUSSI) **et** de l'IDENTITÉ de l'endpoint GPU RunPod ComfyUI (l'id du pod `t68akvx80wv092` + le tag « Flux dev fp8 » cités dans les fiches BIM/Rendu + le master prompt, recomputés du bullet §1 de `AGENTS_EXISTING_ASSETS.md` ; toute fiche mentionnant RunPod doit citer l'id EXACT — token backtické de forme pod == canonique — + le tag EXACT ; un pod re-créé mord les 3 fiches, la disparition totale échoue AUSSI) **et** des évènements d'émission e-CF (ENSEMBLE des évènements émetteurs « réservation/contrat » ⨯ ensemble exclu des brouillons `lead/visite/devis` **disjoint** des émetteurs — jamais de facture sur brouillon — ⨯ ENSEMBLE des `base_field` ⨯ rôle unique `compta-fiscaliste-ecf` double-ancré RBAC portail compta ⨯ FormaPago défaut `3`/Tarjeta/Cardnet #10, recomputés de `ecf_plan.json`) **et** de l'identité d'infrastructure VPS (IP `153.75.250.214` + les DEUX conteneurs Docker `erpnext-backend-1`/`erpnext-frontend-1` recomputés de CLAUDE.md §VPS ; direction copie→source — toute doc `.md` du mandat, hors journaux `05_activity_log`/`daily_reports`, citant une IPv4 ou un conteneur de forme `<projet>-<service>-1` doit == la valeur canonique : une migration serveur ou un renommage du projet compose mord toutes les copies périmées, disparition totale de la surface échoue AUSSI) == artefact cité (#6) | ✅ oui |
|
||
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
||
|
||
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||
`bash` + `git` + `python3` (déjà présents sur un runner standard).
|
||
|
||
### Rejouer le gate EN LOCAL — `run_ci.sh` (racine du dépôt)
|
||
|
||
Avant de committer, reproduire l'intégralité du gate de merge en une commande :
|
||
|
||
```
|
||
bash run_ci.sh # tout gate.needs (7 gates statiques + 23 suites de module)
|
||
bash run_ci.sh --static # seulement les 7 gates ci/*.sh (rapide)
|
||
bash run_ci.sh --list # liste les jobs qui seraient lancés, sans exécuter
|
||
bash run_ci.sh --help # aide + codes de sortie (0 vert · 1 échec · 2 option · 3 hors git)
|
||
```
|
||
|
||
`run_ci.sh` n'a **aucune liste en dur** : il DÉRIVE l'ensemble des checks depuis
|
||
`gate.needs` de `.gitea/workflows/ci.yml` (la source de vérité que
|
||
`check_ci_integrity.sh` verrouille déjà) et exécute, pour chaque job, la
|
||
commande `run:` déclarée dans son `working-directory:`. Ajouter/retirer un gate
|
||
dans ci.yml se répercute donc **automatiquement**, sans éditer le lanceur — ce
|
||
qui élimine la dérive silencieuse des boucles `for g in … ; do bash ci/$g.sh`
|
||
écrites à la main de session en session. Par construction il ne balaie que
|
||
`gate.needs`, ce qui **exclut** les jobs manuels `workflow_dispatch`
|
||
(ex. `e2e-baseline`/`npm test`) et le job `gate` lui-même. Code de sortie 0 ssi
|
||
tous les jobs passent (même sémantique que le gate).
|
||
|
||
Ce n'est **pas un gate** (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) :
|
||
c'est un lanceur de commodité, hors du glob `ci/*.sh` (donc hors périmètre
|
||
[INV-B] de `check_ci_integrity`). Son exactitude est **structurelle** (il exécute
|
||
ce que ci.yml déclare) et n'a pas à être re-gatée (CLAUDE.md #5).
|
||
|
||
## 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 » recompté depuis `git ls-files 03_agents/*/AGENT.md` (source unique)
|
||
et exigé identique à **7 surfaces vives** : `README.md` ×2 (nav + titre) ·
|
||
`AGENTS_EXISTING_ASSETS.md` (titre) · `02_master_prompt/MASTER_PROMPT_DTP_v2.md`
|
||
(titre) · `GAP_ANALYSIS_SPRINT1.md` ×3 (périmètre d'audit · constat global *total*
|
||
— le « 12 » analytique n'est PAS ancré · coche de couverture DoD).
|
||
|
||
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. **2ᵉ surface prose (fiche ONAPI/Legal)** : la fiche
|
||
`03_agents/onapi_legal/AGENT.md:54` ÉNUMÈRE à la main « les **7 entités** de
|
||
CLAUDE.md #Entités (…) » — un COMPTE littéral + la liste complète que le motif (e),
|
||
ancré sur le README `legal/confotur`, ne balaie pas. On re-dérive la liste de
|
||
CLAUDE.md §Entités et on exige de la fiche : **(a)** le compte littéral == |§Entités|
|
||
· **(b)** l'énumération == §Entités **ordre exact** (absent ET fantôme mordus par
|
||
set-diff) · **(c)** l'ancre « CLAUDE.md #Entités » citée · **(d)** cross-check —
|
||
l'énumération == options `entite_porteuse` de l'artefact byte-gaté (la fiche décrit
|
||
**exactement** le menu que le DocType offrira).
|
||
|
||
**Champs `dossier_vente`/`estado` du DocType CONFOTUR dérivés du workflow vente.**
|
||
La §« Cœur du livrable : cross-cohérence » du même README `legal/confotur` affirme
|
||
que **deux** champs du DocType sont **dérivés** du pipeline vente, jamais réécrits
|
||
en dur : `dossier_vente` (Link) → cible `workflow.document_type` (= `OTO Dossier
|
||
Vente`) et `estado` (Select) → options = les états `confotur_*` du **workflow
|
||
vente**. Source faisant autorité : `crm/workflow_vente/workflow_vente_spec.json` —
|
||
l'**input** que le builder confotur lit (déclaré `workflow_source` dans le
|
||
MANIFEST), d'où il tire `document_type` et les `update_value` d'états commençant par
|
||
`confotur`. Les blocs CONFOTUR amont gatent le **compte** de champs/rôles, la
|
||
cross-cohérence des **permissions** et l'ancrage des **entités** (`entite_porteuse`)
|
||
— mais **aucun** ne touche `dossier_vente`/`estado`. Piège #6 : **renommer** le
|
||
DocType porteur côté workflow (`OTO Dossier Vente`→`OTO Dossier de Vente`) ou
|
||
**ajouter/renommer** un état CONFOTUR régénère l'artefact confotur (`check_artifacts`
|
||
reste **vert** — il se reconstruit depuis le spec workflow) **mais** laisse la
|
||
**prose** README « (= `OTO Dossier Vente`) » périmée **en silence** → l'agent
|
||
ONAPI/Legal câblerait une cible de Link / un menu d'états que le pipeline ne porte
|
||
plus. Même classe que l'ancrage `entite_porteuse` ci-dessus. GATE : **(0)**
|
||
traçabilité — le MANIFEST `workflow_source` cite bien le spec workflow ; **(a)**
|
||
cross-artefact — `dossier_vente.options` (Link) == workflow `document_type` ;
|
||
**(b)** prose — la valeur README « (= `…`) » == workflow `document_type` ; **(c)**
|
||
cross-artefact — `estado.options` == `update_value` des états `confotur_*` du
|
||
workflow (**ordre** du pipeline) ; **(c′)** toute option `estado` porte le préfixe
|
||
`confotur` ; **(d)** prose — le bullet `estado` **déclare** la dérivation
|
||
(`confotur_*` + workflow vente). Un claim absent échoue **aussi** (#6). Renommer le
|
||
`document_type` côté spec seul mord **simultanément** l'artefact **et** la prose.
|
||
|
||
**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**.
|
||
|
||
**Fiche agent SEO — 2 métriques data-derived** (`03_agents/seo/AGENT.md`) : le
|
||
tableau « Livrable réellement produit » de la fiche restitue, **à côté** du compte
|
||
de mots-clés (lui déjà gaté sur README **et** fiche), deux sorties byte-gatées mais
|
||
**aveuglées côté fiche** — les blocs SEO schema.org / hreflang précédents ne lisent
|
||
que le **README** (`seo/README.md`), jamais la fiche. L25 « graphe JSON-LD :
|
||
**10 nœuds** (`Organization` + 9 `Residence`) » et L26 « **10 pages** × FR/EN/ES +
|
||
`x-default` » sont recomputées des **mêmes** artefacts que les blocs README
|
||
(`seo_schema_org.json`/`MANIFEST.json` et `seo_hreflang.json`, byte-gatés par
|
||
`check_artifacts`) : L25 — total nœuds == `schema_org_nodes`, racine == le `@type`
|
||
**singleton** du graphe, M == `listings`, type listing == le `@type` **uniforme**
|
||
des listings, plus la cohérence arithmétique `total == 1 + M` ; L26 — nombre de
|
||
pages == `|pages|` de la carte + jeu de langues de contenu == celui de l'artefact
|
||
(set-diff : absent ET en trop). **Renommer** le `@type` des listings,
|
||
**ajouter/retirer** un projet (→ N nœuds & N pages dérivent) ou **échanger** une
|
||
langue de contenu périme la fiche pendant que les trois artefacts disent autre
|
||
chose — même classe que la cellule **chat_otoia** de la fiche Frontend Console
|
||
(fiche-agent qui restitue des attributs data-derived gatés seulement en **agrégat**,
|
||
mémoire `agent-fiche-role-attrs-ungated`). Un claim supprimé é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.
|
||
|
||
**Non-surface documentée — `roles_couverts` (« 44 ») N'EST PAS cross-gaté, à dessein.**
|
||
Le total « 44 rôles couverts par les 5 portails » apparaît, identique, dans **trois**
|
||
manifestes (`frontend/portails`, `frontend/chat_otoia`, `mobile/app_config` ·
|
||
`counts.roles_couverts=44`). On pourrait croire à trois rendus indépendants réclamant
|
||
un cross-check façon RBAC « 50 » (où `rbac/apply_plan/rbac_apply_plan.py:149` exige
|
||
`fixtures==userperm==roleprofile`, car chacun **re-dérive** son décompte avec sa propre
|
||
logique). Ce n'est **pas** le cas ici : les trois modules **importent la même** fonction
|
||
de partition — `mobilelib/deps.py:77` et `chatlib/deps.py:84` font tous deux
|
||
`roles_by_portail = portails_builder.roles_by_portail` (l'unique définition,
|
||
`frontend/portails/wslib/builder.py:36`) — appliquée à l'**unique** contrat
|
||
`rbac/rbac_50_roles.json`. `roles_couverts` est donc **transitivement mono-sourcé**
|
||
(code partagé + donnée partagée) : les trois totaux ne peuvent diverger que si cette
|
||
fonction ou ce contrat change, auquel cas les trois changent ensemble et
|
||
`check_artifacts` (reproductibilité byte-à-byte) vérifie chaque rebuild. Un gate
|
||
« 44==44==44 » serait **redondant** (#5) — cf. mémoire *fix-vs-gate-transitively-
|
||
protected-constant*. Chaque « 44 » vif reste néanmoins ancré à SON manifeste
|
||
(mobile README ci-dessus · `demo/scenarios/out/run_sheet.md` sourcé à
|
||
`portails/…/roles_couverts`), ce qui suffit : la valeur ne peut mentir que si son
|
||
manifeste ment, et le manifeste est byte-gaté.
|
||
|
||
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.
|
||
|
||
Troisième surface du **même README** de l'audit 5D : la **table « ## Les 5
|
||
dimensions »** énumère **à la main**, par ligne, le triplet (`id`, `titre`,
|
||
`reference`) de chaque dimension du cadre (`| **D1** | Traçabilité & anti-invention |
|
||
ISA 500 — Éléments probants |` …). Le tout **premier** bloc audit 5D ne gate que le
|
||
**compte** (« 5 dimensions ») — un **wildcard aveugle à QUELLE dimension** :
|
||
**renommer** un libellé (« Traçabilité & anti-invention » → …) ou **permuter une
|
||
norme de référence** (D3 fiscal citant la mauvaise `Ley 32-23`, D1 probatoire citant
|
||
`ISA 501` au lieu d'`ISA 500`) laisserait le compte à 5 et **passerait en silence** —
|
||
alors que c'est l'audit de **second niveau** que l'auditeur/banquier lit : une norme
|
||
de conformité périmée est une **affirmation fausse** (l'invention même que #6
|
||
interdit). On RECOMPUTE la liste **ordonnée** `{id:(titre, reference)}` depuis
|
||
`qa/audit_5d/out/audit_report.json` (`dimensions[]`, byte-gaté) et on exige que la
|
||
table README l'énumère **exactement** : `id` par **ensemble** (absent **et** en trop
|
||
mordus), **ordre** préservé (D1→D5), puis `(titre, reference)` **par ligne**.
|
||
Cohérence croisée en bonus : les dimensions du rapport == celles de `audit_spec.json`
|
||
(l'INPUT dont le rapport dérive) — mord un builder qui renommerait/permuterait une
|
||
dimension en aval du spec. Table disparue = échec (traçabilité #6).
|
||
|
||
**4ᵉ surface du même README** de l'audit 5D : la section **« ## Livrables audités
|
||
(source = leur `out/`) »** énumère **à la main** les **5 hand-off `out/*.json`** que
|
||
l'audit de **second niveau** LIT comme matière première (`crm/workflow_vente/out/
|
||
workflow.json` … `legal/confotur/out/doctype_confotur_application.json`). Les trois
|
||
blocs audit 5D ci-dessus gatent la **synthèse** (contrôles/dimensions/verdict),
|
||
l'énumération des **open items** et l'identité des **5 dimensions** — **aucun** ne
|
||
touche la liste des **artefacts sources**. Cette liste vit dans
|
||
`qa/audit_5d/out/MANIFEST.json` (`audited_artifacts`, byte-gaté par `check_artifacts`
|
||
⇒ faisant autorité : le générateur la recompute à chaque build via
|
||
`sorted(ARTIFACT_PATHS.values())`, le registre des hand-off amont que
|
||
`load_artifacts()` charge réellement). Le README réénumère ces 5 chemins **dans un
|
||
ordre différent** du MANIFEST (workflow 2ᵉ ici, 3ᵉ au MANIFEST trié) ⇒ comparaison
|
||
**par ensemble**. Piège : **brancher** un nouveau livrable amont sur l'audit
|
||
(nouvelle entrée `ARTIFACT_PATHS` → nouveau chemin dans `audited_artifacts`) ou en
|
||
**retirer** un laisse la section README **périmée en silence** pendant que l'audit
|
||
lit un autre jeu de fichiers — le lecteur/banquier croirait l'audit couvrir un
|
||
périmètre qu'il ne couvre plus (l'invention #6 interdit), qu'aucune suite `tests/`
|
||
(qui teste des FONCTIONS de contrôle, pas la prose) n'attrape. On RECOMPUTE
|
||
l'ensemble depuis `audited_artifacts` et on exige que les **puces** README l'énumèrent
|
||
**exactement** (absent **et** fantôme mordus). Cohérences croisées bonus : **(a)**
|
||
chaque chemin audité **résout** un fichier commité sur disque (le `_load` de l'auditeur
|
||
lève `FileNotFoundError` sinon — audit à vide impossible) ; **(b)** le **nombre** de
|
||
livrables audités == le nombre d'**alias distincts** d'artefacts référencés par les
|
||
`controls[].artifacts` du `audit_spec.json` (un artefact chargé mais jamais contrôlé,
|
||
ou un alias contrôlé sans chemin de chargement → `KeyError` runtime, désynchronise les
|
||
deux). Section absente = échec (traçabilité #6).
|
||
|
||
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).
|
||
|
||
**3ᵉ surface `devops/deploy_runbook` — imputabilité des confirmations** : la ligne
|
||
« Confirmations préalables VPS » du README annote chaque confirmation réglementaire
|
||
d'un **couple** data-derived `nom` (**owner** · **réf-contrôle**), ex.
|
||
`taux_commission` (Direction · audit_5d D1.1). L'**owner** = qui doit confirmer le
|
||
paramètre avant déploiement ; la **réf** = le contrôle `qa/audit_5d` (`D#.#`) — ou la
|
||
session — qui en assure le suivi. Le bloc de comptes ci-dessus ne gate que le
|
||
**nombre** de confirmations et l'**ensemble de leurs noms** — aveugle à **qui** porte
|
||
chaque confirmation et à **quel contrôle** la trace. On recompute owner + réf des
|
||
`confirmations` de `out/deploy_runbook.json` (byte-gaté) **croisées** avec le rapport
|
||
`qa/audit_5d/out/audit_report.json` (byte-gaté), dont chaque contrôle porte son propre
|
||
`owner`. Cross-anchor à trois : owner run-book **compatible** avec l'owner du contrôle
|
||
audit_5d cité (sous-ensemble de tokens · tolère le suffixe éditorial « / UAF ») ·
|
||
owner README ⊆ owner run-book (tolère l'abréviation « Fiscaliste eCF » ⊂ « Compta
|
||
Fiscaliste eCF ») · réf README == contrôle réel (présent **dans audit_5d ET dans la
|
||
source**). Réaffecter `taux_commission` de Direction à Ventes (mauvais responsable
|
||
escaladé), misattribuer le contrôle (`D1.1`→`D2.1`) ou citer un contrôle **fantôme**
|
||
(`D9.9`, absent d'audit_5d) mord — « vert trompeur » qu'aucune suite `tests/`
|
||
(FONCTIONS de graphe) n'attrape. Une confirmation tracée par un contrôle **doit
|
||
rester annotée** (disparition = régression de traçabilité #6) ; une annotation d'un
|
||
nom **fantôme** échoue aussi.
|
||
|
||
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.
|
||
|
||
Ce gate de COMPTE reste toutefois **aveugle à CE QUE dit chaque config runtime**.
|
||
La MÊME cellule « Métrique vérifiée » de la fiche `frontend_console` (`03_agents/
|
||
frontend_console/AGENT.md:29`) restitue en plus TROIS **attributs** data-derived du
|
||
montage — « persona **Amélie** … · langues **FR/EN/ES** · `` `endpoint: null` `` » —
|
||
qu'aucun gate ne recomputait sur CETTE fiche. (Le gate persona/capabilities
|
||
« ancré sur CLAUDE.md §Architecture cible » — voir plus bas — vise
|
||
le spec, les 5 mounts, le MANIFEST, le README et l'oracle du test — **jamais** cette
|
||
cellule de fiche, et il ignore langues comme endpoint.) On recompute donc les trois
|
||
depuis les artefacts byte-gatés `out/chat_mount.json` (une config par portail, 5) et
|
||
`out/MANIFEST.json` : **persona** == `MANIFEST.persona.nom` (« Amélie » ; le « QC »
|
||
est le qualificatif de marque `Voix Amélie QC` de CLAUDE.md, gardé en littéral non
|
||
gaté), **langues** `FR/EN/ES` == `MANIFEST.langues` en majuscule, et **`endpoint:
|
||
null`** — une **affirmation d'honnêteté #6/#8** : l'endpoint OTOIA n'est PAS fabriqué
|
||
(renseigné côté VPS). Le gate exige que CHAQUE mount ait `endpoint == null` ; qu'une
|
||
config gagne un endpoint réel dans l'artefact et la fiche « endpoint: null » devient
|
||
un **mensonge en silence** → morsure. Cohérences croisées (bijection du générateur) :
|
||
`MANIFEST.persona.nom` et `MANIFEST.langues` doivent valoir EXACTEMENT ce que porte
|
||
CHAQUE mount (un montage désynchronisé de son propre manifeste mord), et
|
||
`endpoint_statut == 'a_confirmer'` doit correspondre à des endpoints TOUS `null`
|
||
(sinon incohérence interne de l'artefact). Un claim absent échoue AUSSI. Même classe
|
||
de « vert trompeur » que la fiche mobile / les fiches faisabilité : une suite `tests/`
|
||
teste les FONCTIONS du builder, jamais la prose de la fiche.
|
||
|
||
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.
|
||
|
||
Une **troisième surface** du même README est la colonne **« Fait vérifié »** de la
|
||
table du barème : elle NOMME les deux **seuils structurels** que le scorer applique —
|
||
`DOC` « un `*.md` ≥ **400 octets** » et `TESTS` « ≥ **8 méthodes** `def test_*` ».
|
||
Ces deux nombres sont DATA-DERIVED de `quality_spec.json[thresholds]`
|
||
(`min_doc_bytes`/`min_test_methods`) — l'INPUT du générateur, byte-gaté par
|
||
`check_artifacts` qui prouve que `quality_report.json` se rebuild depuis lui — et
|
||
TRANSCRITS À LA MAIN. Le bloc barème ne capturait QUE le **poids** (2e cellule),
|
||
aveugle à la 3e : élever `min_test_methods` (8→12 : un module de 8-11 tests
|
||
tomberait sous `FAIL`) ou changer `min_doc_bytes` rebuild le report SANS TOUCHER au
|
||
README → prose « ≥ 8 méthodes »/« ≥ 400 octets » périmée en silence, l'auditeur
|
||
documentant un barème qu'il n'applique plus. On recompute les deux seuils depuis le
|
||
spec et on exige que les cellules DOC/TESTS les nomment EXACTEMENT ; **cross-check
|
||
d'ancrage** : l'evidence `TESTS` de CHAQUE module du report porte « (seuil N) »
|
||
(émise par `crit_tests`) — on exige `N == spec.min_test_methods` pour tous, prouvant
|
||
que le seuil ÉCRIT == le seuil réellement APPLIQUÉ (mord une divergence
|
||
spec↔report). 5 morsures vérifiées : README `DOC 400→450` · README `TESTS 8→10` ·
|
||
README ligne DOC vidée (claim absent) · spec `min_test_methods 8→12` (README périmé
|
||
+ cross-check spec↔report) · spec `min_doc_bytes 400→500` ; 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.
|
||
|
||
Autre surface du MÊME livrable `crm/commissions`, la plus grave possible : sa
|
||
**promesse ANTI-INVENTION (#6)** — la RAISON D'ÊTRE du module. Aucun taux de
|
||
commission n'étant documenté dans CLAUDE.md, le README §« Anti-invention » promet
|
||
(README:22-23) que « le barème livré porte `taux_pct: null` + `source: null` +
|
||
`a_confirmer: true` pour **chaque** évènement », et l'invariant CLI #7 REFUSE tout
|
||
`taux_pct` fourni **sans `source`**. La cross-cohérence ci-dessus ne gate QUE
|
||
brouillons/champs de base ; RIEN ne confrontait cette PROMESSE à l'artefact. Or
|
||
c'est la dérive la plus dommageable du module : qu'un `taux_pct` DEVIENNE non-null
|
||
(taux INVENTÉ, ou valeur remplie SANS `source`) pendant que le README affirme
|
||
encore « tout `null` » → le calcul `base × taux` émettrait une commission fondée
|
||
sur un chiffre inventé (l'invention EXACTE que le module promet d'écarter). On
|
||
RECOMPUTE l'état null/confirmé depuis `commission_plan.json` (byte-gaté) et on
|
||
exige que la promesse du README le REFLÈTE **dans les deux sens** (si la Direction
|
||
confirme un jour un taux, la promesse « tout null » DOIT être mise à jour, sinon
|
||
ROUGE — le gate suit la vie du module sans faux négatif). Cohérences croisées :
|
||
invariant #7 permanent (aucun `taux_pct` sans `source`) · `a_confirmer` ⟺
|
||
`taux_pct is null` · `MANIFEST.counts` recoupe le plan (`taux_a_confirmer` == nb à
|
||
confirmer · `evenements` == |plan|). 5 morsures vérifiées : `taux_pct` rempli SANS
|
||
source (invariant #7 · invention) · `taux_pct` rempli AVEC source (a_confirmer
|
||
resté « à confirmer » + promesse README périmée) · `a_confirmer=false` sur taux
|
||
null · `MANIFEST.counts.taux_a_confirmer` périmé (5→4) · promesse anti-invention
|
||
retirée du README (INTROUVABLE) ; restauré = vert.
|
||
|
||
Troisième surface du MÊME livrable `crm/commissions`, le « **à quel rôle** » de la
|
||
question fondatrice : la §Cross-cohérence du README promet que chaque `role_id`
|
||
commissionné « doit être résolu depuis `rbac_50_roles.json` … ET appartenir au
|
||
portail `ventes` » (invariant CLI #5) avec un `erpnext_role_name` « cohérent avec
|
||
RBAC » (invariant #6 · le « nom Frappe résolu » du README:36). C'est une
|
||
**séparation des pouvoirs** : on ne commissionne QUE des rôles du portail ventes.
|
||
Les deux blocs commissions ci-dessus gatent les états/champs/taux, mais RIEN ne
|
||
confrontait cette appartenance-portail ni la résolution du nom à l'artefact. On
|
||
RECOMPUTE depuis `commission_plan.json` + `rbac_50_roles.json` + `MANIFEST.json`
|
||
(byte-gatés · zéro dup du contrat du générateur) : (a) chaque `role_id` payé ∈
|
||
contrat (jamais fantôme) · (b) `erpnext_role_name` du plan == contrat (nom Frappe
|
||
non figé en dur) · (c) portail des rôles payés = **singleton** ∈ `portails_business`
|
||
== le portail `ventes` nommé au README (payer un rôle compta/construction romprait
|
||
le singleton ; renommer le portail au contrat désaccorderait le littéral README) ·
|
||
(d) `MANIFEST.roles_rbac_utilises` == l'ensemble distinct `(role_id, nom)` réellement
|
||
payé, `counts.roles` recoupé. 6 morsures vérifiées : `role_id` fantôme absent du
|
||
contrat · `erpnext_role_name` dérivé dans le plan · rôle **compta** commissionné
|
||
(pouvoirs mélangés) · littéral portail README `ventes→sales` · `roles_rbac_utilises`
|
||
amputé · renommage du rôle DANS le contrat (cascade nom + MANIFEST) ; restauré = vert.
|
||
|
||
Quatrième surface du MÊME livrable `crm/commissions`, le « **sur quelle devise** » du
|
||
calcul : la §Cross-cohérence promet (README:54) que « `devise_field` = le champ
|
||
`devise` (`Select` **USD/DOP** · #10) du DocType ». C'est l'invariant CLI #8 : le plan
|
||
porte la clé racine `devise_field == "devise"`, le DocType Dossier Vente
|
||
(`doctype_spec.json`) porte bien un champ `devise` de fieldtype `Select`, et ses
|
||
`options` == les DEUX devises canoniques de **CLAUDE.md #10** (« **USD + DOP**
|
||
devises »). Les trois blocs commissions ci-dessus gatent états/champs/rôles/taux, mais
|
||
RIEN ne touchait ce couple champ↔devises — surface distincte du bloc « Fiches #10 »
|
||
(prose ERPNext/CRM) et du bloc e-CF `TipoMoneda` (options du spec fiscal). Dérive #6 :
|
||
RENOMMER le champ (`devise`→`moneda`), changer son fieldtype (`Select`→`Data` :
|
||
énumération PERDUE, montant en monnaie libre) ou altérer ses `options` (`USD/DOP`→
|
||
`USD/EUR` : devise HORS #10) ferait dériver la prose en silence pendant que
|
||
l'artefact/DocType dit autre chose — dérive qu'aucune suite `tests/` (FONCTIONS
|
||
finance/résolution, jamais la prose vs artefact) n'attrape. On RECOMPUTE depuis
|
||
`commission_plan.json` + `dossier_vente/doctype_spec.json` (byte-gatés) + CLAUDE.md
|
||
#10 : (0) ANCRAGE — les 2 devises depuis le segment gras « USD + DOP » de #10 · (a)
|
||
CROSS-ARTEFACT — `plan.devise_field == "devise"` == fieldname du champ DocType,
|
||
fieldtype `Select`, `options` (ORDRE inclus) == devises #10 · (b) PROSE — README:54
|
||
nomme le champ `devise`, le dit `Select`, cite `USD/DOP` == #10 et tague `#10`. 7
|
||
morsures vérifiées : README champ `devise→moneda` · README `Select→Data` · README
|
||
`USD/DOP→USD/EUR` · README tag `#10` retiré (INTROUVABLE) · **DocType** `options
|
||
USD→EUR` (hors #10) · **CLAUDE.md #10** `USD+DOP→USD+EUR` (ancrage vivant · cascade) ·
|
||
**plan** `devise_field devise→moneda` (invariant #8) ; 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.
|
||
|
||
**2ᵉ surface du MÊME module** : trois copies DE PLUS des 6 marqueurs, longtemps
|
||
ungated (dont le commentaire ci-dessus NOMMAIT le défaut sans le fermer). (c)
|
||
`genlib/renderer.py::_CANONICAL_MARKERS` — la LISTE que `check_canonical()` parcourt
|
||
pour PROUVER que le rendu du volet ingénierie porte les 6 marqueurs : l'oracle de
|
||
l'enforcement lui-même. Si elle dérive de `CANONICAL` (donc de #9/#10) le contrôle
|
||
devient FAUX (exige un marqueur périmé ⇒ faux rouge ; ou on en retire un ⇒ vert
|
||
vacux). (d) les DEUX docstrings de `genlib/scorer.py` (barème axe 4 + fonction
|
||
`_score_parametres_canoniques`) qui énumèrent « 3 % / 8.5 % / 52 % · USD+DOP · Letter
|
||
US · Cardnet ». (e) l'oracle du test `tests/test_generator.py` (`for marker in [...]`).
|
||
On RÉUTILISE `exp` (zéro duplication) : les LISTES (c)/(e) sont parsées et comparées
|
||
à l'ensemble EXACT (absent ET en trop) ; les DOCSTRINGS (d) doivent porter CHAQUE
|
||
valeur normalisée (espaces/accents-insensibles ⇒ `USD+DOP`==`USD + DOP`). Morsures
|
||
vérifiées : renderer `Cardnet→Azul` (absent/en-trop) · test oracle retire `8.5 %`
|
||
(absent) · scorer docstring `52 %→55 %` (drift) · **CLAUDE.md #10 `Cardnet→Azul`
|
||
mord SIMULTANÉMENT model.py + renderer + test + les DEUX docstrings** (la valeur
|
||
même de l'ancrage) ; 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.
|
||
|
||
Enfin, sur une **3e surface d'appartenance** encore DISTINCTE des deux précédentes :
|
||
l'appartenance à `roles_allowed` du **chat OTOIA** — QUI peut **invoquer** l'assistant
|
||
embarqué dans un portail. Ce contrôle d'accès ne vit ni dans `rbac_50_roles.json`
|
||
(attributs) ni dans `workspace.json` (`Has Role`, accès console) mais dans
|
||
`frontend/chat_otoia/out/chat_mount.json` (une config runtime par portail, byte-gaté
|
||
par `check_artifacts`), et deux fiches l'affirment EN PROSE : `03_agents/rendu/AGENT.md:36`
|
||
— rôle « OTO Faisabilité Rendu 3D » **fait partie des `roles_allowed` du portail
|
||
`construction`** (droit d'invoquer le chat pour déclencher les rendus assistés) ; et
|
||
l'assertion **NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle
|
||
« OTO Plateforme Mobile » (famille `plateforme`) **volontairement dans AUCUNE
|
||
`roles_allowed`** (le chat ne couvre que les portails métier). Aucun gate ne les
|
||
couvrait : le bloc Chat OTOIA amont gate l'**identité** de la persona/capabilities
|
||
(ancrée CLAUDE.md) et la **composition** (5 mounts ⇔ 5 portails), aveugle à QUEL rôle
|
||
figure dans quelle `roles_allowed` ; le bloc Fiches Workspace gate un artefact + une
|
||
sémantique DIFFÉRENTS (accès console ≠ droit d'invocation du chat). On RECOMPUTE, depuis
|
||
`chat_mount.json`, l'ensemble des portails dont la `roles_allowed` porte
|
||
l'`erpnext_role_name` du rôle (résolu du contrat par `id`, zéro duplication · #6), et on
|
||
confronte l'assertion : **in_named_portail** (rendu) — le portail nommé est data-derived
|
||
(== le `portail` du rôle au contrat, **triple ancrage** : portail nommé == `portail`
|
||
contrat == unique portail dont la `roles_allowed` le porte) et membership == {ce portail}
|
||
ET la prose le cite avec le droit « invoquer le chat OTOIA » ; **not_in_any** (mobile) —
|
||
rôle ABSENT de TOUTE `roles_allowed`. Un portail nommé mais **absent** des mounts (typo)
|
||
échoue avant tout (anti-cible fantôme). 6 morsures vérifiées (3 côté artefact, 3 côté
|
||
fiche/bord) : artefact retire le rôle rendu de la `roles_allowed` construction (perte du
|
||
déclenchement assisté) · artefact ajoute le rôle mobile à `ventes` (**SUR-EXPOSITION** du
|
||
chat #6 VIOLÉE) · artefact ajoute AUSSI rendu à `compta` (droit élargi, la fiche nomme UN
|
||
seul portail) · fiche rendu nomme le mauvais portail (`construction→compta`, INTROUVABLE) ·
|
||
fiche mobile retire le token `roles_allowed` de l'assertion négative (INTROUVABLE) ;
|
||
restauré = vert.
|
||
|
||
**Tokens design canoniques `publiciste/branding` ancrés sur CLAUDE.md #4** : la
|
||
contrainte NON-NÉGOCIABLE #4 fixe l'identité visuelle luxury — fond dark `#0a0a12`
|
||
+ accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond**. Ces 4
|
||
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.
|
||
|
||
**Pied de page « Voix Amélie QC (multilingual_v2) » des fiches agents ancré sur
|
||
CLAUDE.md §Architecture cible.** 11 des 13 fiches `03_agents/*/AGENT.md` (toutes sauf
|
||
`faisabilite`/`publiciste`, qui n'orchestrent pas OTOIA) se terminent par « - Voix
|
||
**Amélie QC** (**multilingual_v2**) pour toute interaction OTOIA », recopiant à la main
|
||
les **trois** tokens de CLAUDE.md §Architecture cible « Voix Amélie QC (multilingual_v2) » :
|
||
le prénom persona (`Amélie`), le qualificatif de marque (`QC`) et le modèle de voix
|
||
ElevenLabs (`multilingual_v2`). Le bloc persona ci-dessus re-dérive bien persona+voix de
|
||
CLAUDE.md **mais ne confronte que le module `chat_otoia`** (spec + `out/` + son propre
|
||
README + l'oracle du test) — **aveugle aux 11 pieds de page** ; le bloc « configs
|
||
runtime » note même que le « QC » est « gardé en littéral non gaté ici ». Piège #6 : le
|
||
jour où ElevenLabs déprécie `multilingual_v2` (→`multilingual_v3`) ou qu'on renomme la
|
||
persona/le qualificatif dans CLAUDE.md, ces 11 fiches annonceraient **en silence** une
|
||
voix/persona que le mandat ne prescrit plus, chacune affirmant orchestrer « toute
|
||
interaction OTOIA » avec le mauvais modèle — aucune suite `tests/` (fonctions du builder,
|
||
jamais la prose des fiches) ne l'attrape. On **re-dérive indépendamment** le triplet
|
||
(persona · qualif · voix) de CLAUDE.md §Architecture cible (regex qui ne fige **pas** le
|
||
qualificatif en dur — un renommage `QC→…` mord ici, pas seulement via l'INTROUVABLE du
|
||
bloc persona) et on exige de **chaque** fiche portant le pied de page que ses trois
|
||
tokens == CLAUDE.md ; une fiche avec « interaction OTOIA » mais pied de page malformé
|
||
échoue, et la **disparition totale** du recensement échoue aussi (régression #6).
|
||
**6 morsures vérifiées** : CLAUDE.md `multilingual_v2→v3` (mord les 11 fiches +, en
|
||
cascade, le bloc persona amont — l'ancre est vive) · CLAUDE.md qualif `QC→BC` (mord les
|
||
11 pieds de page, bloc indépendant) · fiche `seo` voix `v2→v1` · fiche `crm` persona
|
||
`Amélie→Sophie` · fiche `bim` pied de page malformé (parenthèses retirées) · pied de
|
||
page retiré de **toutes** les fiches (surface évaporée) ; 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**.
|
||
|
||
**Compte canonique de rôles RBAC ancré sur CLAUDE.md §Architecture cible.** Le
|
||
mandat **fixe** « **RBAC 50 rôles** » (CLAUDE.md §Architecture cible). Ce nombre
|
||
canonique est recopié — **sans jamais être re-dérivé du mandat** — dans le contrat
|
||
`rbac_50_roles.json` (le champ **déclaré** `cible_rbac_roles` ET la **taille réelle**
|
||
du catalogue `len(roles)`), dans les schémas (`const: 50` de `fixtures.schema.json` /
|
||
`userperm.schema.json`) et dans la fiche agent `03_agents/erpnext_backend/AGENT.md`
|
||
(« RBAC 50 rôles natif » · « **50 rôles** » · « RBAC **50 rôles** exactement » · « RBAC
|
||
50 rôles · L38 »). Les gates RBAC amont (roleprofile/apply_plan/userperm) ne prouvent
|
||
que la **cohérence INTERNE** — chaque README de volet == son `MANIFEST`, lui-même
|
||
recompté du contrat — **aveugles** à l'ancrage au **mandat** : changer « RBAC 50 rôles »
|
||
dans CLAUDE.md, ou faire dévier le catalogue de 50 de façon **interne-cohérente**
|
||
(`cible_rbac_roles` ET `len(roles)` alignés sur une autre valeur, tous les MANIFEST
|
||
recomptés, generator vert), laisserait le contrat et la fiche affirmer « 50 exactement »
|
||
en **contredisant le mandat**, en silence — même « vert trompeur » que les tokens
|
||
branding #4 ou la persona OTOIA §Architecture cible, qu'aucune suite `tests/` (FONCTIONS
|
||
de génération, jamais l'ancre à CLAUDE.md) n'attrape. On **re-dérive** `N` de CLAUDE.md
|
||
§Architecture cible (source unique · bloc entre « ## Architecture cible » et le `##`
|
||
suivant) et on exige : **(a)** contrat `cible_rbac_roles` == N ; **(b)** taille réelle
|
||
`len(roles)` == N ; **(c)** cible == réel (le mandat est **atteint** par le catalogue) ;
|
||
**(d)** la fiche porte l'invariant emphatique « RBAC **N rôles** exactement » ET
|
||
**aucune** mention « M rôles » périmée (M != N · « RBAC 60 **tests** » n'est PAS une
|
||
mention de rôles). **5 morsures vérifiées** : CLAUDE.md « RBAC 50→60 rôles » (**ancre
|
||
vive** — mord a+b sur le contrat resté 50 ET la fiche en cascade) · contrat
|
||
`cible_rbac_roles` 50→49 (déviation déclarée · mord a+c) · contrat **retrait d'un rôle**
|
||
(`len` 50→49 · cible reste 50 · mord b+c) · fiche retire l'invariant « exactement »
|
||
(claim absent = régression #6) · fiche mention 50→48 (mention périmée) ; restauré = vert
|
||
(6 checks · exit 0) · working tree **byte-restauré** (`git checkout --`,
|
||
**JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**Comptes du run-book d'application (`RBAC_50_ROLES_SPEC.md` §7).** La NOTE §7
|
||
(« ✅ … est **livré** en-repo ») restitue, sur une **surface DISTINCTE** des READMEs
|
||
`userperm_gen` / `roleprofile_gen` (déjà gatés) et du §3 (ventilation par portail,
|
||
bloc `1quater`), **trois comptes data-derived** du run-book d'application : « **28**
|
||
templates `Company` (portée `entite`) », « **6 profils** (**5** portails métier +
|
||
console technique `plateforme`) » et « **50 rôles de façon bijective** ». Ces nombres
|
||
sont **recopiés à la main** dans la SPEC et n'étaient couverts par **aucun** gate sur
|
||
cette surface. On les **recompute** depuis le contrat `rbac_50_roles.json` (source de
|
||
vérité byte-gatée par `check_artifacts`) : **28** == |`scope_donnees == entite`| (=
|
||
nombre de `User Permission` templates), **6** == |portails distincts| (1 `Role Profile`
|
||
par portail), **5** == portails **métier** (hors `plateforme`, la console technique),
|
||
**50** == |rôles| (bijection). **Bumper** une portée `entite`→`groupe` dans le contrat
|
||
(le décompte de templates change) ou **ajouter/retirer** un portail (le décompte de
|
||
profils change) périme la note **en silence** pendant que les READMEs de volet restent
|
||
verts — le vrai « vert trompeur » (bloc `1quinquies`). Le blockquote markdown `> ` de
|
||
la note survit au collapse de `read_norm` et fragmente la prose ; il est **neutralisé**
|
||
avant match. **6 morsures vérifiées** : SPEC `28→27` (prose périmée) · SPEC
|
||
`6 profils→7` · SPEC `5 portails métier→4` · SPEC `50→49` · **contrat** une portée
|
||
`entite→groupe` (28→27 · README stale = *silent green*) · SPEC retire « 28 templates »
|
||
(claim absent = INTROUVABLE) ; restauré = vert (exit 0) · working tree **byte-restauré**
|
||
(`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**Colonne « Entité » de la §4 ancrée sur CLAUDE.md §Entités (`RBAC_50_ROLES_SPEC.md`
|
||
§4).** La table §4 « Cartographie rôles ↔ entités » liste, en **colonne de tête**, les
|
||
entités porteuses (`entite_principale`) du RBAC : les **7 entités NOMMÉES** du mandat
|
||
(`WAF` · `WA SRL` · `AC Arias Cuevas` · `Consortium ECR DR` · `Helios RD` · `Ploutos` ·
|
||
`9060 QC`) + le **périmètre consolidé** « Groupe ». Cette colonne a **deux sources
|
||
faisant autorité** — (i) **CLAUDE.md §Entités** (l'orthographe + le descripteur
|
||
parenthétique de chaque entité nommée) et (ii) `rbac_50_roles.json` (l'**ENSEMBLE** des
|
||
`entite_principale` réellement portées par les 50 rôles) — mais **aucun** gate ne la
|
||
touchait : le §3 gate les comptes par **portail** (`1quater`), le §7 les comptes du
|
||
run-book (`1quinquies`), les blocs fiches l'`entite` d'**UN** rôle ; tous **aveugles à
|
||
l'identité de CETTE liste**. On la couvre (bloc `1sexies`) : **(a) ENSEMBLE** des
|
||
entités de §4 == distinct `entite_principale` du contrat (set-diff : ni fantôme ni
|
||
manquante) ; **(b) ANCRAGE** — chaque entité NOMMÉE de CLAUDE.md §Entités est une ligne
|
||
de §4 avec son **descripteur parenthétique EXACT** (absence comprise) ; **(c)** chaque
|
||
entité nommée du mandat est **portée ≥ 1×** par le contrat (`cm_ents ⊆ contrat`, aucune
|
||
entité décorative). **AJOUTER** au contrat un rôle porté par une entité absente de §4,
|
||
**RENOMMER** une entité dans CLAUDE.md §Entités (`9060 QC→9061 QC`) ou **DÉRIVER** un
|
||
descripteur laisse la SPEC contredire le mandat/le contrat **en silence** — même « vert
|
||
trompeur » que les entités CONFOTUR (§Entités) ou la marque SEO org. **8 morsures
|
||
vérifiées** : **contrat** ajoute un rôle porté par une entité absente de §4 (`(a)`
|
||
manquante) · **contrat** renomme toutes les entités `Ploutos→Ploutos SRL` (`(a)` fantôme
|
||
`Ploutos` + manquante `Ploutos SRL` · `(c)` `Ploutos` décorative) · **CLAUDE.md**
|
||
`9060 QC→9061 QC` (`(b)` absent + `(c)` décorative) · **CLAUDE.md** descripteur
|
||
`(holding)→(holding co)` (`(b)` mismatch) · **SPEC §4** `AC Arias Cuevas→AC Arias`
|
||
(`(a)` fantôme+manquante + `(b)` absent) · **SPEC §4** descripteur `marque publique→
|
||
marque privée` (`(b)` mismatch) · **SPEC §4** retire la ligne « Groupe » (`(a)`
|
||
manquante) · **SPEC** supprime l'en-tête `## 4.` (section INTROUVABLE) ; restauré = vert
|
||
(exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) ·
|
||
**7 gates re-verts**.
|
||
|
||
**Ensemble des portails métier + exclusion nommée de la console technique
|
||
(`03_agents/frontend_console/AGENT.md`).** Le bloc « triplets par workspace » recompute
|
||
cartes/liens/rôles **par** portail mais reste **aveugle à QUELS portails** composent
|
||
l'ensemble. La fiche l'énumère à la main en **deux endroits** data-derived : L11
|
||
« **5 portails rôle métier** (Ventes / Construction / Achat / Compta / Direction) » et
|
||
L46 « la console technique `plateforme` (**6ᵉ portail RBAC**) est **exclue** ». On les
|
||
couvre (bloc « Portails ensemble ») : **(a)** l'**ensemble ORDONNÉ des noms courts** ==
|
||
les workspaces métier de `frontend/portails/out/MANIFEST.json` byte-gaté (nom court =
|
||
label « OTO <Nom> » sans le préfixe, comme l'énumération des onglets Mobile) **ET, DOUBLE
|
||
ANCRAGE**, leurs ids == `portails_business` du **contrat RBAC** (l'« égalité stricte »
|
||
que la fiche revendique elle-même en L47) ; **(b)** `plateforme` == l'**UNIQUE** portail
|
||
RBAC **hors métier** (∈ portails distincts des rôles ∖ `portails_business`), et c'est le
|
||
**6ᵉ** (total distinct == 6 = 5 métier + 1). Un **ÉCHANGE** (Achat→Marketing), un
|
||
**RETRAIT**, un **RÉORDRE**, un **RENOMMAGE** de la console technique dans le contrat
|
||
(`plateforme`→`console`) ou un contrat qui **PROMEUT** `plateforme` en métier laisse la
|
||
fiche contredire les artefacts **en silence** — même patron que l'énumération des onglets
|
||
Mobile ou la colonne « Entité » de la §4. **8 morsures vérifiées** : **fiche L11** échange
|
||
`Achat→Marketing` (`(a)` en trop/absent) · **fiche L11** retire `Compta` (`(a)` compte +
|
||
absent) · **fiche L11** réordonne `Ventes↔Construction` (`(a)` ordre) · **fiche L46**
|
||
`plateforme→console` (`(b)` nom) · **fiche L46** `6ᵉ→7ᵉ` (`(b)` rang) · **fiche L46**
|
||
retire `**exclue**` (claim INTROUVABLE) · **contrat** réordonne `portails_business`
|
||
(ancrage rompu) · **contrat** renomme `ventes→sales` dans `portails_business` (ancrage
|
||
rompu + `plateforme` n'est plus l'unique portail hors métier) ; restauré = vert (exit 0) ·
|
||
working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · **7 gates
|
||
re-verts**.
|
||
|
||
**IDENTITÉ de l'endpoint GPU RunPod ComfyUI ancrée sur l'inventaire maître
|
||
(`03_agents/{bim,rendu}/AGENT.md` + `02_master_prompt/`).** Les fiches BIM/Rendu et le
|
||
master prompt NOMMENT, entre backticks, l'ID du pod RunPod ComfyUI qui exécute le rendu
|
||
Flux (`t68akvx80wv092`) + le tag du modèle (« Flux dev fp8 ») — **6 citations SAISIES à
|
||
la main** dont la source de vérité in-repo est le bullet §1 de `AGENTS_EXISTING_ASSETS.md`
|
||
(« le fichier maître » que les fiches citent explicitement comme leur ancre), **hors de
|
||
tout gate** : `check_docs` ne voit qu'un lien, `guard_constraints` qu'un terme interdit,
|
||
`check_artifacts` qu'un `out/` — un ID de pod n'est aucun des trois. Or un pod RunPod est
|
||
**RE-CRÉÉ avec un NOUVEL id** (le cas le plus courant) : l'inventaire se met à jour, les
|
||
citations se **PÉRIMENT en silence** → l'agent Rendu/BIM câblerait le batch Flux vers un
|
||
**endpoint GPU MORT** (l'invention même que #6 interdit). On RECOMPUTE l'id + le tag depuis
|
||
le bullet maître (jamais une liste à la main) ; **direction fiche→inventaire** : toute
|
||
fiche/master prompt qui **mentionne RunPod** doit citer l'id **EXACT** (tout token backtické
|
||
de forme pod — alphanum minuscule ≥12 avec un chiffre — == canonique) **et** le tag Flux
|
||
**EXACT** ; la disparition **totale** de la surface échoue AUSSI. **6 morsures vérifiées** :
|
||
**inventaire** re-crée le pod (id change → mord les **3 fiches** en cascade, id périmé) ·
|
||
**inventaire** bump le tag Flux (`dev fp8→schnell fp8` → tag périmé dans les 3) · **fiche
|
||
rendu** mistype une citation (id fantôme) · **fiche bim** retire le tag Flux (tag absent) ·
|
||
**master prompt** retire l'id backtické (citation incomplète) · **inventaire** supprime le
|
||
bullet canonique (ancre morte, recompute impossible) ; restauré = vert (exit 0) · working
|
||
tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**IDENTITÉ des évènements d'émission e-CF (le « cœur du livrable » fiscal) ancrée sur
|
||
`ecf_plan.json`.** La section §Cross-cohérence de `fiscal/ecf_dgii/README.md:40-59` — « le
|
||
cœur du livrable » — transcrit **à la main** l'identité des `emission_events` de l'artefact
|
||
byte-gaté : l'**ENSEMBLE** des évènements qui émettent un e-CF (« seulement réservation et
|
||
contrat »), l'**ENSEMBLE exclu** des brouillons (« on ne facture pas un brouillon
|
||
(lead/visite/devis) »), l'ENSEMBLE des `base_field` (`montant_reservation`/`montant_contrat`),
|
||
le **rôle unique** (`compta-fiscaliste-ecf` · « OTO Compta Fiscaliste eCF » · portail compta)
|
||
et la **FormaPago défaut** (« = 3 (Tarjeta) » · Cardnet #10). Le seul gate fiscal existant
|
||
couvre le **format e-NCF** + le compte de tests ; le bloc racine, le seul agrégat « e-CF 39
|
||
tests ». **Rien** ne confrontait cette identité à l'artefact. Dérive la plus grave (#6/#5) :
|
||
**ajouter un `emission_event` sur un état BROUILLON** (ex. `devis`) — l'anti-pattern EXACT
|
||
que le module interdit (« on ne facture pas un brouillon ») — pendant que le README dit encore
|
||
« seulement réservation et contrat » → l'agent ERPNext Backend câblerait une **facture émise
|
||
avant soumission/encaissement**. On RECOMPUTE les ENSEMBLES depuis `ecf_plan.json` (jamais une
|
||
liste à la main) et exige que le README les reflète **EXACTEMENT** (set-diff : ni fantôme ni
|
||
manquant ; accents/casse normalisés) ; le rôle est **double-ancré** sur `rbac_50_roles.json`
|
||
(id → portail compta → nom ERPNext concordant) ; l'ensemble exclu doit rester **DISJOINT** des
|
||
émetteurs (jamais de facture sur brouillon). Cohérences croisées en bonus : rôle **unique** sur
|
||
tous les évènements · défaut ∈ **catalogue** FormaPago · ensembles non vides. **6 morsures
|
||
vérifiées** : artefact facture un brouillon (`devis` émetteur → mismatch d'ensemble **ET**
|
||
clash de disjonction) · artefact renomme un `base_field` · artefact change la FormaPago défaut
|
||
`3→1` · artefact casse le rôle unique (2ᵉ évènement → autre rôle) · README périmé « seulement
|
||
contrat » (réservation retiré) · motif du défaut ne cite plus « Cardnet » (#10) ; restauré =
|
||
vert (exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) ·
|
||
**7 gates re-verts**.
|
||
|
||
Un claim **absent** échoue aussi (la dérive de formulation qui ferait
|
||
disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur
|
||
(bash/git/python3), zéro réseau.
|
||
|
||
### `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.
|