c2222c83b6
Le README `demo/scenarios` porte une table « Scénario | Projet | Audience | Angle »
(README:10-13). Le bloc « Démo » existant de check_readme_claims ne gate QUE
counts.modules_cites_uniques (le compte de modules du diagramme) — ses trois colonnes
d'identité étaient des WILDCARDS, et les libellés projets sont déclarés (README:91-92)
« proviennent verbatim de CLAUDE.md · §Projets » sans AUCUN ancrage vérifié.
Ces colonnes sont DATA-DERIVED de out/run_sheet.json.scenarios[] (id · projet ·
projet_libelle · audience), byte-gaté par check_artifacts. Le générateur ne saisit
aucune donnée métier (résolution RFC 6901 depuis le disque) ; mais check_artifacts
ne prouve QUE run_sheet==build (byte-for-byte) et la source scenario_spec.json (map
projets) recopie les libellés À LA MAIN → toute la chaîne peut DÉRIVER de CLAUDE.md
en restant byte-verte. PREUVE : classer P07 sous audience=client dans le README →
check_readme_claims EXIT 0 (le gate ne voyait que 10==10 modules). Un couple
projet/audience faux ferait pitcher au présentateur le mauvais scénario — le risque
même que la run-sheet veut éliminer — « vert trompeur » qu'aucune suite tests/ (qui
teste des FONCTIONS de résolution, pas la prose) n'attrape.
Nouveau bloc « Démo scénarios » dans ci/check_readme_claims.sh : (1) ANCRAGE — chaque
`P{code} {libellé}` du run_sheet == entrée « ## Projets » de CLAUDE.md (verbatim ·
source unique · même esprit que le catalogue projets dossier_vente) ; (2) TABLE —
chaque ligne porte EXACTEMENT id + `P{code} {libellé}` + audience (fin du wildcard ·
patron identique à la colonne « Type » de roleprofile_gen) ; (3) IDENTITÉ d'ensemble
— {ids des lignes} == {ids du run_sheet} == counts.scenarios (aucune ligne FANTÔME,
aucun scénario MANQUANT). Cohérences croisées en bonus (mordent un artefact
INTERNEMENT incohérent) : run_sheet ↔ MANIFEST d'accord sur (id,projet,audience) ·
id == S-{projet}-{AUDIENCE} · counts.scenarios == |scenarios| · ids non vides/sans
doublon. Un claim absent échoue AUSSI.
8 morsures vérifiées : prose renomme un id (INTROUVABLE) · prose classe P07 sous
audience=client · prose met le mauvais libellé projet · ligne FANTÔME S-P99-GHOST
(en trop) · artefact libellé « Aqua Terra Bay » DÉRIVE de CLAUDE.md · run_sheet↔
MANIFEST désync audience · counts.scenarios=3 (≠|scenarios|=2) · ligne S-P05-CLIENT
supprimée (MANQUANT) ; restauré = green : 2 lignes == run_sheet == counts (2) ·
libellés == CLAUDE.md §Projets · exit 0. État courant : aucune valeur périmée
(anti-invention #6, rien à réécrire) — le défaut est la surface ungated. ci/README.md
(table + détail « 2e surface demo/scenarios ») mis à jour · working tree byte-restauré
· 7 gates re-verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
667 lines
53 KiB
Markdown
667 lines
53 KiB
Markdown
# CI/CD · OTO Enterprise OS DTP · Gitea Actions
|
||
|
||
> **Livrable Sprint 1** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` §Sprint 1 · DevOps)
|
||
> _« CI/CD Gitea Actions »_ — gate qualité qui conditionne tous les sprints suivants.
|
||
>
|
||
> **Plateforme : Gitea Actions UNIQUEMENT** (CLAUDE.md contrainte #2 · JAMAIS GitHub).
|
||
|
||
---
|
||
|
||
## 1. Ce que fait le pipeline
|
||
|
||
Workflow : `.gitea/workflows/ci.yml`. Déclenché sur `push` / `pull_request` vers `main`
|
||
et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` par module) :
|
||
|
||
| Job | Script | Rôle | Blocant |
|
||
|---|---|---|---|
|
||
| `constraints-guard` | `ci/guard_constraints.sh` | Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md | ✅ oui |
|
||
| `validate-json` | `ci/validate_json.sh` | Parse strict de tous les `*.json` (schémas Faisabilité↔Publiciste) | ✅ oui |
|
||
| `check-docs` | `ci/check_docs.sh` | Liens Markdown internes + présence auto-score 4Big | ✅ oui (liens) |
|
||
| `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui |
|
||
| `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui |
|
||
| `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui |
|
||
| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) == artefact cité (#6) | ✅ oui |
|
||
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
||
|
||
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||
`bash` + `git` + `python3` (déjà présents sur un runner standard).
|
||
|
||
## 2. Détail des contrôles
|
||
|
||
### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES
|
||
Détecte l'**usage** (pas la simple mention) de :
|
||
- Plateformes git interdites : `github.com`, `gitlab.com`, `bitbucket.org` (#2).
|
||
- CRM interdits : `EspoCRM`, `HubSpot` (#3).
|
||
- Paiement interdit : `Stripe` (#10 · Cardnet only).
|
||
- Écriture directe dans `/var/www/html/static/` (Interdit absolu).
|
||
- Commande `git clean` (Interdit absolu).
|
||
- Remote git pointant ailleurs que Gitea/interne.
|
||
|
||
**Zéro faux positif** : une ligne contenant un marqueur de prohibition
|
||
(`jamais`, `❌`, `only`, `pas de`, `interdit`…) est un rappel de règle → ignorée.
|
||
Escape hatch documenté : ajouter `ci-allow` sur une ligne pour l'exclure.
|
||
|
||
### `validate_json.sh`
|
||
Parse chaque `*.json` suivi (dont `version.schema.json` et
|
||
`projets_master.schema.json`, contrat de données Faisabilité → Publiciste).
|
||
Un JSON cassé casse le pipeline aval → attrapé ici.
|
||
|
||
### `check_docs.sh`
|
||
- **[HARD]** liens Markdown relatifs internes : la cible doit exister.
|
||
- **[SOFT]** livrables `05_deliverables_mvp/*.md` : mention d'auto-score 4Big
|
||
attendue (≥95/100, CLAUDE.md #5). Avertissement seul, non blocant.
|
||
|
||
### `check_artifacts.sh`
|
||
Régénère chaque artefact `05_deliverables_mvp/**/out/` versionné depuis son
|
||
générateur (`build -o <tmp>`) et exige que tout fichier produit soit (a) **suivi
|
||
par git** — jamais un artefact seulement sur disque (`out/` `.gitignore`-é ou
|
||
non-`git add`), qui serait **absent en CI propre** et rendrait le gate « vert en
|
||
local » par artefact fantôme (même classe que le bug `regression_run.json`) — et
|
||
(b) en **égalité byte-for-byte** avec le fichier commité. Découverte automatique
|
||
(zéro liste à la main · #6) : tout module avec un `out/` et un générateur `build`
|
||
entre dans le gate.
|
||
|
||
Cible la **dérive silencieuse** : un module auto-résout des valeurs depuis les
|
||
artefacts d'**autres** modules (ex. `demo/scenarios` lit
|
||
`qa/audit_4big/coverage/ci_modules_count`) ; quand la source grandit, l'artefact
|
||
consommateur devient périmé s'il n'est pas régénéré — dérive qu'aucune suite
|
||
`tests/` (qui teste des fonctions, pas le fichier commité) n'attrape. Correctif :
|
||
`build -o out` puis commit. Les artefacts d'**exécution** non produits par `build`
|
||
(p.ex. `qa/regression/out/regression_run.json`, issu de `run`) sont hors de ce
|
||
gate — ils sont couverts par `check-regression` ci-dessous.
|
||
|
||
### `check_regression.sh`
|
||
Rejoue la **matrice de régression** complète (`qa/regression/regression_gen.py run`,
|
||
~5 s, stdlib pur) vers un tmp et exige que le `regression_run.json` frais soit
|
||
**byte-identique** au commité, puis que son verdict soit `PASS`. Complément direct
|
||
de `check_artifacts` : celui-ci ne rejoue que `build` (→ `regression_plan.json`) et
|
||
laisse hors périmètre l'artefact d'**exécution** `regression_run.json` — pourtant
|
||
c'est *lui* qui porte le compte agrégé (suites · tests · passés · verdict) cité dans
|
||
la doc et les logs — recompté par `check_readme_claims` dans les fiches d'entrée,
|
||
jamais figé en dur ici. Sans ce gate, ce compte pouvait se périmer en
|
||
silence (module + job CI ajoutés sans régénérer la matrice → compte de suites faux ;
|
||
c'est la dérive « demo 18→21 » corrigée à la main), ou une matrice rouge être
|
||
commitée verte. `regression_run.json` ne contient aucun horodatage/hôte → le run est
|
||
déterministe et l'égalité exacte licite. Correctif : `regression_gen.py run -o out`
|
||
puis commit.
|
||
|
||
### `check_ci_integrity.sh`
|
||
Prouve, en lisant `.gitea/workflows/ci.yml`, que le **câblage** du workflow tient —
|
||
car le job `gate` est le **seul verrou de merge** : un check absent de son `needs:`
|
||
ne bloque **rien**, même rouge. Deux invariants :
|
||
- **INV-A** — `gate.needs` == { tous les jobs définis } − { `gate` } − { jobs manuels }
|
||
(un job manuel = gardé par `if: … workflow_dispatch …`, ex. `e2e-baseline`,
|
||
légitimement hors du gate push/PR car il exige un serveur live). Détecte un job
|
||
**oublié** du gate (MISSING → ne bloque pas), une **référence fantôme** (DANGLING →
|
||
typo / job renommé-supprimé) et un job **manuel** glissé dans `needs` (gate en
|
||
attente perpétuelle sur push).
|
||
- **INV-B** — chaque script `ci/*.sh` du dépôt est **câblé** : soit lancé par un job
|
||
(`run: bash ci/<script>`) lui-même dans `gate.needs`, soit **sourcé** par ≥1 gate
|
||
script (lib partagée `ci/lib.sh` — jamais un job propre). Un nouveau gate statique
|
||
**non câblé** (script mort) ou **décâblé**, ou une **lib morte** (sourcée par
|
||
personne), casse le check.
|
||
|
||
Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
||
`qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de
|
||
**module** (ceux portant un `working-directory:`) — les gates **statiques** sans
|
||
working-directory n'étaient gardés par personne. Même classe d'anti-dérive que INV4
|
||
(disque→CI), appliquée au **câblage** CI. stdlib pur (bash/awk/git), zéro réseau.
|
||
|
||
### `check_readme_claims.sh`
|
||
Le `README.md` est le **point d'entrée** du mandat ; sa section « État courant
|
||
(sourcé) » affiche des chiffres et déclare *« Chaque chiffre ci-dessous est sourcé
|
||
vers un artefact commité (anti-invention #6) ; ce README n'introduit aucune donnée
|
||
nouvelle. »* Cette promesse n'était gardée par **aucun** gate : `check_docs.sh` ne
|
||
valide que les **liens** (la cible existe), jamais la **valeur** des nombres. Quand un
|
||
module + son job CI sont ajoutés (21→22 modules, 21→22 suites) ou qu'une promesse
|
||
change de statut (14 `in_repo` + 1 `out_of_scope` → 15 `in_repo`), les chiffres du
|
||
README se **périment en silence** tout en restant « sourcés » vers un artefact qui dit
|
||
autre chose — un README qui se **contredit avec sa propre source** est un « vert
|
||
trompeur » (même classe que la matrice périmée « demo 18→21 » ou INV4, appliqué à la
|
||
doc d'entrée). Ce gate **recompute** chaque chiffre depuis l'artefact cité (jamais une
|
||
liste à la main · #6) et exige l'égalité avec ce qui est **écrit** dans les trois
|
||
docs d'entrée — `README.md` :
|
||
- modules gated `N/M` à `K/100` + verdict `PASS` → `qa/audit_4big/out/quality_report.json` (`totals`) ;
|
||
- `N` suites gated → `qa/regression/out/regression_plan.json` (`totals.suites`) ;
|
||
- `N` promesses (`X` sprint + `Y` métriques), `Z` `in_repo`, verdict `true` → `qa/acceptance/out/acceptance_matrix.json` ;
|
||
- « 13 agents » (×2 : nav + titre) → `git ls-files 03_agents/*/AGENT.md`.
|
||
|
||
Et la fiche QA `03_agents/qa/AGENT.md` (section « Verdict agrégé courant », qui se
|
||
disait « jamais compté à la main » tout en portant un compte figé qui s'est périmé
|
||
— « 21 suites · 534 tests » alors que le run agrégé faisant autorité en disait davantage) :
|
||
- `N` suites · `M` tests · `M` passés · `E` échec · `E` erreur · verdict → `qa/regression/out/regression_run.json` (compte agrégé faisant autorité, commité).
|
||
|
||
Et la fiche ERPNext Backend `03_agents/erpnext_backend/AGENT.md` (ligne « source unique »,
|
||
même compte agrégé jadis saisi à la main qui s'était périmé — « 560 tests ») :
|
||
- `N` tests · `M` suites · verdict → `qa/regression/out/regression_run.json` (matrice de régression du repo, même source faisant autorité que la fiche QA).
|
||
|
||
Et — au grain le **plus fin** — la colonne « Tests » des **tables de livrables** de
|
||
**toutes** les fiches (`03_agents/*/AGENT.md` · crm, faisabilite, publiciste, qa) :
|
||
chaque cellule par suite était saisie à la main et se périmait dès qu'un test était
|
||
ajouté (fiche QA : `acceptance` 31 alors que la suite en portait 37 · `audit_4big`
|
||
35→34 · `regression` 25→26). Le compte **agrégé** ci-dessus (« 564 tests ») ne
|
||
suffit pas : une **compensation** entre deux suites (+1 / −1) laisserait la somme
|
||
juste et les deux lignes fausses. On recompute donc chaque cellule depuis
|
||
`qa/regression/out/regression_plan.json` (`suites[path].test_methods`) — et depuis
|
||
`reglib.discovery.count_tests` (même fonction que le plan · zéro duplication) pour le
|
||
`self_module` `qa/regression`, exclu de la matrice par SoD mais bien documenté.
|
||
|
||
Enfin — même classe, un cran **au-dessus** des cellules — les agrégats rédigés **en
|
||
prose** dans deux fiches (`crm` : « Total CRM **81 tests** (25 + 31 + 25) » ;
|
||
`erpnext_backend` : « **RBAC 60 tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests** »).
|
||
Le **total ET le multiset des composants** (ordre-indépendant) sont recomputés depuis
|
||
`suites[path].test_methods` : une **compensation** entre deux suites laisserait la
|
||
table (gatée ci-dessus) juste et la prose fausse — c'est le même piège que les
|
||
cellules, au niveau agrégé.
|
||
|
||
Même classe encore, hors du domaine « tests » : les comptes **par workspace**
|
||
(cartes / liens / rôles) des portails frontend, transcrits **à la main** dans
|
||
**deux** docs — la table du README du module (`frontend/portails/README.md`) **et**
|
||
la prose de la fiche agent (`03_agents/frontend_console/AGENT.md` : « `OTO Ventes`
|
||
4 cartes/11 liens/12 rôles · … »). Chaque triplet **et** le total « N `Workspace`
|
||
natifs » sont recomputés depuis `frontend/portails/out/MANIFEST.json` (tableau
|
||
`workspaces`, lui-même byte-gaté par `check_artifacts`) : ajouter un rôle/DocType
|
||
au contrat RBAC ferait dériver **les deux** docs en silence sinon.
|
||
|
||
Même classe enfin sur les **comptes RBAC des 3 volets** (`rbac/roleprofile_gen`,
|
||
`rbac/apply_plan`, `rbac/userperm_gen`), recopiés **à la main** dans leurs READMEs
|
||
de module : la table « Nb rôles » **par profil** (+ total bijectif 50), la ligne
|
||
agrégée « Génération réelle : 6 étapes · 50 rôles / 116 DocPerm / 28 UP templates
|
||
/ 6 Role Profile · 4 DocType custom + 5 Company + 4 rôles equipe » (8 comptes de
|
||
tous les volets en une phrase) et la ventilation « 28 `entite` · 16 `groupe` · 2
|
||
`own` · 4 `equipe` ». Chaque nombre est recomputé depuis le `out/MANIFEST.json`
|
||
du volet (recalculé du contrat `rbac_50_roles.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) : ajouter un rôle/DocType ferait dériver **les trois** READMEs
|
||
en silence sinon. **2ᵉ surface `rbac/roleprofile_gen`** : la table « Profils
|
||
générés » ne gatait que le **compte** `Nb rôles` par ligne — sa colonne « Type »
|
||
(`métier`/`technique`) était un **wildcard** (`[^|]*`), donc un portail technique
|
||
classé « métier » (ou l'inverse) passait **vert**. On recompute le classement
|
||
depuis le flag booléen `metier` de `MANIFEST.profiles` et on exige la colonne
|
||
« Type » **exacte** par profil, on gate la prose « **6 profils** (5 métier + 1
|
||
technique) » contre `counts.role_profiles/portails_metier/portails_techniques`, on
|
||
exige que le **nombre de lignes de profil** de la table == `|profiles|` (aucune
|
||
ligne fantôme d'un profil inventé absent du contrat), et en bonus deux
|
||
cohérences croisées qui mordent un MANIFEST **internement** incohérent :
|
||
`portails_metier + portails_techniques == role_profiles` **et** ces comptes
|
||
recomptent les flags `metier` réels des profils.
|
||
|
||
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**.
|
||
|
||
**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).
|
||
|
||
Même classe encore sur l'**app mobile Expo** : la ligne récap du README du module
|
||
`mobile/app_config` (« **5 onglets** · **44 rôles** couverts · **3 langues** ·
|
||
**13 identifiants `a_confirmer`** ») transcrivait **à la main** un quadruplet dont
|
||
chaque nombre est recompté à chaque build depuis `rbac_50_roles.json` (portails
|
||
métier + rôles), `seo_spec.json` (langues) et la liste `a_confirmer` (credentials
|
||
EAS/stores jamais fabriqués). Les quatre sont recomputés depuis
|
||
`mobile/app_config/out/MANIFEST.json` (`counts.{onglets,roles_couverts,langues,
|
||
identifiants_a_confirmer}`, byte-gaté par `check_artifacts`). Recomputer **les
|
||
quatre** volets mord une dérive de n'importe lequel (ajouter un rôle/portail RBAC,
|
||
une langue, un credential store) là où un seul nombre resterait aveugle. **2ᵉ
|
||
surface du même README** : la même ligne récap **énumère** les 5 onglets
|
||
(« (Ventes/Construction/Achat/Compta/Direction) ») — le patron du compte matche
|
||
la parenthèse par `\([^)]*\)` mais en **ignore le contenu**. Cette liste est
|
||
data-derived : `mobile/app_config/out/role_navigation.json` (byte-gaté) porte un
|
||
onglet par portail métier (bijection 1 onglet ⇔ 1 portail), chaque entrée = son
|
||
`label` « OTO <Nom> ». On recompute la **liste ordonnée** des noms courts et on
|
||
exige que la parenthèse l'énumère **exactement** : un **échange** (Ventes→
|
||
Marketing), un **retrait** (Compta absent — l'énumération tombe à 4 tandis que
|
||
« **5 onglets** » reste juste) ou un **réordre** est mordu là où le seul compte
|
||
est aveugle. Cohérence croisée : `len(role_navigation) == counts.onglets ==
|
||
counts.portails` — mord un compteur du manifeste désynchronisé de sa navigation.
|
||
|
||
Même classe enfin sur le **DocType CONFOTUR Application** (livrable `legal/confotur`) :
|
||
la ligne de synthèse du README du module (« **18 champs** (14 de donnée) · **4
|
||
sections** · **3 rôles** · … · **2 évènements de dépôt** ») **et** la cellule de la
|
||
fiche agent `onapi_legal` transcrivaient **à la main** le même quintuplet, dérivé du
|
||
spec du DocType. Les cinq nombres sont recomputés depuis
|
||
`legal/confotur/out/MANIFEST.json` (`counts.{fields,data_fields,sections,roles,
|
||
depot_events}`, recomptés du `confotur_spec.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) et comparés aux **deux** docs. Recomputer **les cinq** volets mord
|
||
une dérive de n'importe lequel (ajouter un champ/une section/un rôle au spec) là où un
|
||
seul nombre resterait aveugle.
|
||
|
||
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.
|
||
|
||
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 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.
|
||
|
||
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.
|