abc9b62214
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
397 lines
28 KiB
Markdown
397 lines
28 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) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer) **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) **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`) == 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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
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.
|