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