afa61657cb
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1753 lines
134 KiB
Markdown
1753 lines
134 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 **et des faits data-derived** des docs d'entrée (`README.md` « État courant », fiches `03_agents/*/AGENT.md`, READMEs de module) : chaque nombre, label de verdict et identité citée en prose (rôle · DocType · entité · marque · persona…) est **recomputé de l'artefact commité qu'il cite** — ou re-dérivé de `CLAUDE.md` / de la roadmap pour les valeurs ancrées au mandat — puis exigé **exact** (#6), jamais une liste à la main : un doc qui se contredit avec sa propre source est un « vert trompeur ». Surfaces couvertes : comptes de tests/suites, verdicts & barème 4Big, ledgers d'invariants CI, ventilations & identités RBAC, mots-clés & graphe SEO, pipeline vente, e-CF DGII, CONFOTUR, app mobile Expo, run-books VPS, tokens design #4, etc. **Détail exhaustif surface par surface : §2 `check_readme_claims.sh` (ci-dessous).** | ✅ oui |
|
||
| `check-mobile-workflow` | `ci/check_mobile_workflow.sh` | Intégrité du **second** workflow `mobile-build.yml` (hors gate de merge, mais sa STRUCTURE est gatée ici) : gating portable (0 `secrets.` en `if:` de job → pas de SKIP silencieux) · builds gatés sur les outputs `preflight` (activation différée #6/#8) · contrat d'outputs (déclarés == référencés == alimentés). **Détail : §2 `check_mobile_workflow.sh`.** | ✅ 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).
|
||
|
||
### Second workflow Gitea Actions — `mobile-build.yml` (HORS gate de merge)
|
||
|
||
Le dépôt versionne un **second** workflow Gitea Actions,
|
||
[`.gitea/workflows/mobile-build.yml`](../.gitea/workflows/mobile-build.yml)
|
||
(livrable Mobile · roadmap Sprint 5 · CI/CD mobile EAS), qui **n'appartient pas
|
||
au gate de merge** décrit ci-dessus. C'est volontaire et vérifié par construction :
|
||
`check-ci-integrity` ne verrouille **que** `ci.yml` (variable `CI=` du script), et
|
||
**aucun** de ses jobs n'est câblé dans `gate.needs` — il ne conditionne donc aucun
|
||
merge et `run_ci.sh` ne le rejoue pas (hors `gate.needs`). Il tourne sur le **même
|
||
runner** que `ci.yml` et reste **SKIP-safe** : tant que le secret `EAS_TOKEN` n'est
|
||
pas fourni par Michel, ses jobs de build (`eas build`/`eas submit`) sont en SKIP —
|
||
il ne rend **jamais** le CI rouge (`preflight` reste vert et rapporte les secrets
|
||
attendus). Périmètre complet (#8 · exécution serveur hors repo), profils EAS et
|
||
variables `a_confirmer` : [`README` du module Mobile](../05_deliverables_mvp/mobile/app_config/README.md).
|
||
|
||
Nuance importante : ses **jobs** sont hors gate de merge, mais sa **structure de
|
||
fichier**, elle, EST gatée — par le job `check-mobile-workflow`
|
||
([`ci/check_mobile_workflow.sh`](check_mobile_workflow.sh), dans `gate.needs`).
|
||
Sans lui, une régression du workflow passait VERTE en silence (mutation vérifiée :
|
||
réintroduire `secrets.` dans un `if:` de job laissait les 7 gates statiques verts).
|
||
C'est précisément le risque « marche dans ma tête, casse le jour où Michel active
|
||
`EAS_TOKEN` » : le contexte `secrets` n'est pas disponible dans `jobs.<id>.if`
|
||
(table d'availability GitHub Actions), donc un tel `if:` s'évaluerait vide sur
|
||
`act`/Gitea → build **SKIP silencieux** même token fourni. Le gate rend cet
|
||
invariant (et l'activation différée) opposable au merge.
|
||
|
||
### Rejouer le gate EN LOCAL — `run_ci.sh` (racine du dépôt)
|
||
|
||
Avant de committer, reproduire l'intégralité du gate de merge en une commande :
|
||
|
||
```
|
||
bash run_ci.sh # tout gate.needs (gates statiques ci/*.sh + suites de module)
|
||
bash run_ci.sh --static # seulement les gates statiques ci/*.sh (rapide)
|
||
bash run_ci.sh --list # liste les jobs qui seraient lancés (+ décompte dérivé), sans exécuter
|
||
bash run_ci.sh --help # aide + codes de sortie (0 vert · 1 échec · 2 option · 3 hors git)
|
||
```
|
||
|
||
Le **décompte exact** (`N jobs = G gates statiques + S suites de module`) est
|
||
**dérivé de `gate.needs` et imprimé à l'exécution** (et par `--list`) ; il n'est
|
||
**jamais figé dans cette doc**, où il se périmerait en silence à chaque module
|
||
ajouté/retiré — même raison anti-invention (`CLAUDE.md` #6) qui a fait retirer
|
||
ces mêmes comptes du commentaire de `run_ci.sh`.
|
||
|
||
`run_ci.sh` n'a **aucune liste en dur** : il DÉRIVE l'ensemble des checks depuis
|
||
`gate.needs` de `.gitea/workflows/ci.yml` (la source de vérité que
|
||
`check_ci_integrity.sh` verrouille déjà) et exécute, pour chaque job, la
|
||
commande `run:` déclarée dans son `working-directory:`. Ajouter/retirer un gate
|
||
dans ci.yml se répercute donc **automatiquement**, sans éditer le lanceur — ce
|
||
qui élimine la dérive silencieuse des boucles `for g in … ; do bash ci/$g.sh`
|
||
écrites à la main de session en session. Par construction il ne balaie que
|
||
`gate.needs`, ce qui **exclut** les jobs manuels `workflow_dispatch`
|
||
(ex. `e2e-baseline`/`npm test`) et le job `gate` lui-même. Code de sortie 0 ssi
|
||
tous les jobs passent (même sémantique que le gate).
|
||
|
||
Ce n'est **pas un gate** (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) :
|
||
c'est un lanceur de commodité, hors du glob `ci/*.sh` (donc hors périmètre
|
||
[INV-B] de `check_ci_integrity`). Son exactitude est **structurelle** (il exécute
|
||
ce que ci.yml déclare) et n'a pas à être re-gatée (CLAUDE.md #5).
|
||
|
||
## 2. Détail des contrôles
|
||
|
||
### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES
|
||
Détecte l'**usage** (pas la simple mention) de :
|
||
- Plateformes git interdites : `github.com`, `gitlab.com`, `bitbucket.org` (#2).
|
||
- CRM interdits : `EspoCRM`, `HubSpot` (#3).
|
||
- Paiement interdit : `Stripe` (#10 · Cardnet only).
|
||
- Écriture directe dans `/var/www/html/static/` (Interdit absolu).
|
||
- Commande `git clean` (Interdit absolu).
|
||
- Remote git pointant ailleurs que Gitea/interne.
|
||
|
||
**Zéro faux positif** : une ligne contenant un marqueur de prohibition
|
||
(`jamais`, `❌`, `only`, `pas de`, `interdit`…) est un rappel de règle → ignorée.
|
||
Escape hatch documenté : ajouter `ci-allow` sur une ligne pour l'exclure.
|
||
|
||
**Exclusions de fichiers** (`tracked_files()`) — documents qui contiennent
|
||
*nécessairement* les termes interdits en tant que **texte de politique** (mention),
|
||
jamais en usage exécutable : le garde lui-même (il énumère les termes), les workflows
|
||
CI (ils le référencent), les **mandats signés de Michel à la racine**
|
||
(`AUTORISATIONS_*.md`, `DIRECTIVE_*.md`) — prose de gouvernance listant règles ET
|
||
autorisations —, et le **design system de référence** (`OTO_DESIGN_SYSTEM_*.md`,
|
||
root-owned, ajouté par commit `REFERENCE`) dont le §15 « Fichiers canoniques sur VPS »
|
||
est un **inventaire** d'emplacements servis (`/var/www/html/static/`, symlink de <!-- ci-allow : description de l'exclusion, jamais un usage -->
|
||
`/opt/oto/sites/static/`) — mention descriptive, pas une écriture. L'usage *réel* d'un
|
||
terme interdit vivrait dans un `.sh`/`.py`/`.yml`
|
||
(**toujours scannés**), donc aucune détection vraie-positive n'est perdue (mutation-test
|
||
à l'appui). **`CLAUDE.md` reste scanné** : sa prose d'interdits passe via les marqueurs
|
||
de prohibition. NB · `AUTORISATIONS_*.md` l.35 « Écriture dans `/var/www/html/static/` » <!-- ci-allow : citation du mandat -->
|
||
**contredit** l'interdit absolu de CLAUDE.md (le chemin correct est le SYMLINK source
|
||
`/opt/oto/sites/static/`) — contradiction inter-mandats **signalée à Michel** (à trancher
|
||
par lui, pas masquée par le garde). Même inconsistance chemin-**servi**-vs-**source** dans
|
||
`OTO_DESIGN_SYSTEM_v1.md` §15 (liste `/var/www/html/static/` au lieu de la source <!-- ci-allow : citation de l'inconsistance signalée, jamais un usage -->
|
||
`/opt/oto/sites/static/`) — **re-signalée à Michel**, non tranchée par le garde.
|
||
|
||
**Non-dérive documentée — `michel/otov7-platform` (CLAUDE.md #2) N'EST PAS une coquille
|
||
de `michel/oto-enterprise-os-dtp`, à ne PAS « corriger ».** Un audit doc-vs-réalité relève
|
||
périodiquement que la contrainte #2 nomme `michel/otov7-platform` (port 3015) tandis que la
|
||
section *Git* de CLAUDE.md et le README racine nomment `michel/oto-enterprise-os-dtp` — d'où
|
||
un soupçon de faute de frappe dans deux docs qui se contrediraient. **Ce sont deux dépôts
|
||
Gitea distincts, tous deux corrects** : `otov7-platform` est le dépôt du **produit** (la
|
||
plateforme OTOV7 en cours de construction, cible du refactoring), `oto-enterprise-os-dtp` est
|
||
le dépôt de **mandat** (ce worker, ses générateurs et hand-off). La contrainte #2 verrouille
|
||
la plateforme *produit* sur Gitea ; la section *Git* précise où vit le *mandat*. Aucune des
|
||
deux ne ment. **Ne pas « aligner » CLAUDE.md #2 sur le nom du mandat** — ce serait corrompre
|
||
la constitution (mémoire *otov7-platform-not-drift*). Le garde `guard_constraints.sh` ne
|
||
vérifie que l'**absence** de plateformes interdites (github/gitlab/bitbucket), pas l'identité
|
||
d'un repo Gitea autorisé — donc rien à gater ici (#5).
|
||
|
||
### `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. Trois 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.
|
||
- **INV-C** — le workflow force le **mode UTF-8 de Python** (`env: PYTHONUTF8: "1"`
|
||
au niveau workflow → tous les jobs). Motif : chaque générateur clôt son `build`
|
||
par un `print("✅ …" / accents français)` vers stdout, dont l'encodage suit la
|
||
**locale du runner**. Sur une image runner ASCII (`LC_ALL=C` sans coercition
|
||
PEP 538, `PYTHONUTF8` non posé), ce print lève `UnicodeEncodeError` **après**
|
||
écriture des fichiers → `check-artifacts` voit un exit non-nul et les 25 suites
|
||
plantent : **tout le gate passe ROUGE le jour où l'image runner change**, alors
|
||
qu'il est VERT sur le poste dev (UTF-8) — classe « le runner n'est pas ton poste »
|
||
(sœur des sweeps hash-seed / locale-TZ / forward-compat, mais ici un **défaut
|
||
réel présent**, pas une simple attestation). `PYTHONUTF8=1` (PEP 540) impose
|
||
stdout/stdin/défaut-fichier en UTF-8 **partout** → 20 builds qui crashaient +
|
||
les suites redeviennent locale-robustes (prouvé : `run_ci.sh` sous
|
||
`LC_ALL=C` neutralisé = 33 PASS). Comme l'absence de cet `env:` est **invisible**
|
||
sur un runner UTF-8, INV-C l'**ancre** : le retirer casse ce gate au lieu de
|
||
dériver en silence. `run_ci.sh` **exporte la même variable** (miroir fidèle).
|
||
|
||
Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
||
`qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de
|
||
**module** (ceux portant un `working-directory:`) — les gates **statiques** sans
|
||
working-directory n'étaient gardés par personne. Même classe d'anti-dérive que INV4
|
||
(disque→CI), appliquée au **câblage** CI. stdlib pur (bash/awk/git), zéro réseau.
|
||
|
||
### `check_readme_claims.sh`
|
||
Le `README.md` est le **point d'entrée** du mandat ; sa section « État courant
|
||
(sourcé) » affiche des chiffres et déclare *« Chaque chiffre ci-dessous est sourcé
|
||
vers un artefact commité (anti-invention #6) ; ce README n'introduit aucune donnée
|
||
nouvelle. »* Cette promesse n'était gardée par **aucun** gate : `check_docs.sh` ne
|
||
valide que les **liens** (la cible existe), jamais la **valeur** des nombres. Quand un
|
||
module + son job CI sont ajoutés (21→22 modules, 21→22 suites) ou qu'une promesse
|
||
change de statut (14 `in_repo` + 1 `out_of_scope` → 15 `in_repo`), les chiffres du
|
||
README se **périment en silence** tout en restant « sourcés » vers un artefact qui dit
|
||
autre chose — un README qui se **contredit avec sa propre source** est un « vert
|
||
trompeur » (même classe que la matrice périmée « demo 18→21 » ou INV4, appliqué à la
|
||
doc d'entrée). Ce gate **recompute** chaque chiffre depuis l'artefact cité (jamais une
|
||
liste à la main · #6) et exige l'égalité avec ce qui est **écrit** dans les trois
|
||
docs d'entrée — `README.md` :
|
||
- modules gated `N/M` à `K/100` + verdict `PASS` → `qa/audit_4big/out/quality_report.json` (`totals`) ;
|
||
- `N` suites gated → `qa/regression/out/regression_plan.json` (`totals.suites`) ;
|
||
- `N` promesses (`X` sprint + `Y` métriques), `Z` `in_repo`, verdict `true` → `qa/acceptance/out/acceptance_matrix.json` ;
|
||
- « 13 agents » recompté depuis `git ls-files 03_agents/*/AGENT.md` (source unique)
|
||
et exigé identique à **7 surfaces vives** : `README.md` ×2 (nav + titre) ·
|
||
`AGENTS_EXISTING_ASSETS.md` (titre) · `02_master_prompt/MASTER_PROMPT_DTP_v2.md`
|
||
(titre) · `GAP_ANALYSIS_SPRINT1.md` ×3 (périmètre d'audit · constat global *total*
|
||
— le « 12 » analytique n'est PAS ancré · coche de couverture DoD).
|
||
|
||
Et la fiche QA `03_agents/qa/AGENT.md` (section « Verdict agrégé courant », qui se
|
||
disait « jamais compté à la main » tout en portant un compte figé qui s'est périmé
|
||
— « 21 suites · 534 tests » alors que le run agrégé faisant autorité en disait davantage) :
|
||
- `N` suites · `M` tests · `M` passés · `E` échec · `E` erreur · verdict → `qa/regression/out/regression_run.json` (compte agrégé faisant autorité, commité).
|
||
|
||
Et la fiche ERPNext Backend `03_agents/erpnext_backend/AGENT.md` (ligne « source unique »,
|
||
même compte agrégé jadis saisi à la main qui s'était périmé — « 560 tests ») :
|
||
- `N` tests · `M` suites · verdict → `qa/regression/out/regression_run.json` (matrice de régression du repo, même source faisant autorité que la fiche QA).
|
||
|
||
Et — au grain le **plus fin** — la colonne « Tests » des **tables de livrables** de
|
||
**toutes** les fiches (`03_agents/*/AGENT.md` · crm, faisabilite, publiciste, qa) :
|
||
chaque cellule par suite était saisie à la main et se périmait dès qu'un test était
|
||
ajouté (fiche QA : `acceptance` 31 alors que la suite en portait 37 · `audit_4big`
|
||
35→34 · `regression` 25→26). Le compte **agrégé** ci-dessus (« 564 tests ») ne
|
||
suffit pas : une **compensation** entre deux suites (+1 / −1) laisserait la somme
|
||
juste et les deux lignes fausses. On recompute donc chaque cellule depuis
|
||
`qa/regression/out/regression_plan.json` (`suites[path].test_methods`) — et depuis
|
||
`reglib.discovery.count_tests` (même fonction que le plan · zéro duplication) pour le
|
||
`self_module` `qa/regression`, exclu de la matrice par SoD mais bien documenté.
|
||
|
||
Enfin — même classe, un cran **au-dessus** des cellules — les agrégats rédigés **en
|
||
prose** dans deux fiches (`crm` : « Total CRM **81 tests** (25 + 31 + 25) » ;
|
||
`erpnext_backend` : « **RBAC 60 tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests** »).
|
||
Le **total ET le multiset des composants** (ordre-indépendant) sont recomputés depuis
|
||
`suites[path].test_methods` : une **compensation** entre deux suites laisserait la
|
||
table (gatée ci-dessus) juste et la prose fausse — c'est le même piège que les
|
||
cellules, au niveau agrégé.
|
||
|
||
Même classe encore, hors du domaine « tests » : les comptes **par workspace**
|
||
(cartes / liens / rôles) des portails frontend, transcrits **à la main** dans
|
||
**deux** docs — la table du README du module (`frontend/portails/README.md`) **et**
|
||
la prose de la fiche agent (`03_agents/frontend_console/AGENT.md` : « `OTO Ventes`
|
||
4 cartes/11 liens/12 rôles · … »). Chaque triplet **et** le total « N `Workspace`
|
||
natifs » sont recomputés depuis `frontend/portails/out/MANIFEST.json` (tableau
|
||
`workspaces`, lui-même byte-gaté par `check_artifacts`) : ajouter un rôle/DocType
|
||
au contrat RBAC ferait dériver **les deux** docs en silence sinon.
|
||
|
||
Même classe enfin sur les **comptes RBAC des 3 volets** (`rbac/roleprofile_gen`,
|
||
`rbac/apply_plan`, `rbac/userperm_gen`), recopiés **à la main** dans leurs READMEs
|
||
de module : la table « Nb rôles » **par profil** (+ total bijectif 50), la ligne
|
||
agrégée « Génération réelle : 6 étapes · 50 rôles / 116 DocPerm / 28 UP templates
|
||
/ 6 Role Profile · 4 DocType custom + 5 Company + 4 rôles equipe » (8 comptes de
|
||
tous les volets en une phrase) et la ventilation « 28 `entite` · 16 `groupe` · 2
|
||
`own` · 4 `equipe` ». Chaque nombre est recomputé depuis le `out/MANIFEST.json`
|
||
du volet (recalculé du contrat `rbac_50_roles.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) : ajouter un rôle/DocType ferait dériver **les trois** READMEs
|
||
en silence sinon. **2ᵉ surface `rbac/roleprofile_gen`** : la table « Profils
|
||
générés » ne gatait que le **compte** `Nb rôles` par ligne — sa colonne « Type »
|
||
(`métier`/`technique`) était un **wildcard** (`[^|]*`), donc un portail technique
|
||
classé « métier » (ou l'inverse) passait **vert**. On recompute le classement
|
||
depuis le flag booléen `metier` de `MANIFEST.profiles` et on exige la colonne
|
||
« Type » **exacte** par profil, on gate la prose « **6 profils** (5 métier + 1
|
||
technique) » contre `counts.role_profiles/portails_metier/portails_techniques`, on
|
||
exige que le **nombre de lignes de profil** de la table == `|profiles|` (aucune
|
||
ligne fantôme d'un profil inventé absent du contrat), et en bonus deux
|
||
cohérences croisées qui mordent un MANIFEST **internement** incohérent :
|
||
`portails_metier + portails_techniques == role_profiles` **et** ces comptes
|
||
recomptent les flags `metier` réels des profils. **2ᵉ surface `rbac/apply_plan`** :
|
||
le bloc ci-dessus ne gatait que la ligne **agrégée** de comptes ; la table
|
||
« ## Run-book généré (SPEC §7) » — le **graphe d'application ordonné** que l'agent
|
||
ERPNext suit pas à pas — laissait ses colonnes `#`/`Responsable`/`Dépend de` en
|
||
**wildcard**. Or ces trois colonnes sont data-derived de `out/apply_plan.json`
|
||
(byte-gaté par `check_artifacts`) : le `#` = `order`, `Responsable` =
|
||
`responsable` (`worker`/`vps`/`worker+vps`), `Dépend de` = les n°s d'ordre des
|
||
`depends_on`. Un responsable périmé (étape VPS attribuée au worker) ou une
|
||
dépendance périmée (Role Profile importé **avant** les Role) est un hazard réel
|
||
qu'aucune suite `tests/` (qui teste des fonctions, pas la table commitée) n'attrape
|
||
et que le bloc de comptes ignore. On recompute `#`/`responsable`/`deps` depuis
|
||
`apply_plan.json` et on exige que la table les énonce **exactement** (la colonne
|
||
« Étape » reste libre — paraphrase) ; l'**identité d'ensemble** (lignes de la table
|
||
== étapes de l'artefact) mord toute ligne fantôme ou étape manquante, et trois
|
||
cohérences croisées mordent un plan **internement** incohérent : ordres contigus
|
||
`1..N` sans doublon · `responsable` ∈ {`worker`, `vps`, `worker+vps`} · toute
|
||
dépendance pointe en **arrière** (n° < n° de l'étape). **2ᵉ surface
|
||
`rbac/userperm_gen`** : le bloc des comptes ne gatait que la **ventilation** par
|
||
mécanisme (« 28 `entite` · 16 `groupe` · 2 `own` · 4 `equipe` ») — aveugle à
|
||
**quel** mécanisme applique **quelle** portée. La table « Mapping `scope_donnees`
|
||
→ mécanisme » est la **fonction d'enforcement row-level** (cœur sécurité du
|
||
module) : chaque portée → son `mechanism` Frappe natif + le verdict « Template
|
||
émis ? ». Ces trois sont data-derived de `out/user_permission_plan.json`
|
||
(byte-gaté par `check_artifacts`, chaque entrée portant `scope_donnees` +
|
||
`mechanism` + `user_permission_template`). **Réaffecter** une portée à un
|
||
mécanisme plus permissif (`entite` → `none_consolidated` : le row-level enforcement
|
||
**abandonné**, sur-exposition des données inter-entités), **renommer** un mécanisme
|
||
ou **basculer** le verdict « Template émis ? » laisse la table figée pendant que
|
||
l'artefact dit autre chose — hazard réel qu'aucune suite `tests/` (qui teste des
|
||
fonctions de mapping, pas la table commitée) n'attrape et que le bloc de comptes
|
||
ignore. On recompute par portée le `mechanism` (1er token backtické de la colonne)
|
||
et le verdict « Template émis ? » et on exige la table **exacte** ; l'**identité
|
||
d'ensemble** (portées de la table == portées de l'artefact) mord toute ligne
|
||
fantôme ou portée manquante, et trois cohérences croisées mordent un plan
|
||
**internement** incohérent : chaque portée mappe **un seul** mécanisme (fonction,
|
||
pas relation) · le verdict « Template émis » est **uniforme** sur les entrées
|
||
d'une portée · le template est émis **exactement** pour `user_permission_company`
|
||
(l'invariant « template SSI `entite` »).
|
||
|
||
Même classe enfin côté **SEO** : le total et la ventilation par langue des
|
||
mots-clés (« **258** mots-clés (fr=87 · en=87 · es=84) ») étaient transcrits **à
|
||
la main** dans **deux** docs — la table « Livrables » du README de module
|
||
(`05_deliverables_mvp/seo/README.md`) **et** la même ligne de la fiche agent
|
||
(`03_agents/seo/AGENT.md`) — sans aucun gate. Le quadruplet (total + fr/en/es) est
|
||
recomputé depuis `seo/out/MANIFEST.json` (`counts.keywords_total` +
|
||
`counts.keywords_per_lang`, recomptés de `projets_master.json` à chaque build,
|
||
byte-gaté par `check_artifacts`). Recomputer les **trois** langues, pas seulement
|
||
le total, attrape une **compensation** entre langues (fr −1 / en +1) qui laisserait
|
||
le total juste et la ventilation fausse — ajouter un projet ferait sinon dériver
|
||
**les deux** docs en silence. **2ᵉ surface du même README SEO** : la table
|
||
« Livrables » décrit **en prose** la composition du graphe schema.org (« graphe
|
||
JSON-LD (`Organization` + une `Residence` par projet) »). Le bloc des comptes de
|
||
mots-clés est **aveugle** à ces `@type` : **renommer** le type des listings
|
||
(`Residence` → `Apartment`, un `@type` voisin du rango schema.org), **ajouter** un
|
||
3ᵉ type de nœud (`Offer`/`Product`) ou **casser** la bijection (deux nœuds pour un
|
||
projet) laisse la prose figée pendant que `seo/out/seo_schema_org.json` (byte-gaté
|
||
par `check_artifacts`) dit autre chose. On recompute l'**ensemble** des `@type` du
|
||
`@graph` et on exige que la parenthèse les nomme **exactement** (set-diff : absent
|
||
ET en trop ; plus le type « par projet » nommé == le `@type` des listings). En
|
||
bonus, trois cohérences croisées de l'artefact : le nœud racine `Organization`
|
||
(`@id …#organization`) est un **singleton**, les listings sont **uniformes** (un
|
||
seul `@type`) et en **bijection** avec les projets (`listings == projects ==
|
||
schema_org_nodes−1` du MANIFEST), et le graphe ne porte **aucune** `offers` —
|
||
cohérent avec « aucun prix pour un statut sans prix » (USD · #10). Un claim
|
||
supprimé (« composition INTROUVABLE ») échoue **aussi**.
|
||
|
||
**Identité du nœud racine `Organization` ancrée sur CLAUDE.md §Entités.** Le bloc
|
||
composition ci-dessus gate les `@type` et la bijection, jamais l'**identité de la
|
||
marque** que Google indexe. Or le nœud racine `Organization` de
|
||
`seo/out/seo_schema_org.json` porte `name: "Helios RD"` (la **marque publique** ·
|
||
CLAUDE.md §Entités « **Helios RD** (marque publique) sous WAG ») et `url` == le
|
||
base_url du site. La byte-gate `check_artifacts` prouve seulement que l'artefact
|
||
**se reconstruit** depuis `seo_spec.json` — **pas** que ce nom == la marque nommée
|
||
dans CLAUDE.md. Piège #6 : **renommer** la marque dans
|
||
`seo_spec.json[schema_org.organization.name]` (ou dans CLAUDE.md §Entités) fait
|
||
émettre au JSON-LD **public** un nom qui **contredit le mandat** pendant que la
|
||
byte-gate reste **verte** (l'artefact se reconstruit fidèlement… depuis un spec
|
||
dérivé) — même « vert trompeur » que les tokens branding ancrés sur #4. On
|
||
**re-dérive** la marque de CLAUDE.md §Entités (parenthèse « (marque publique) »,
|
||
zéro duplication), le base_url du MANIFEST byte-gaté et le `country_code` du spec,
|
||
puis on exige : **(a)** `Organization.name` == marque CLAUDE.md ; **(b)**
|
||
`Organization.url` == base_url et `@id` sous base_url ; **(c)** chaque nœud listing
|
||
rattaché à **cette** Organization (`brand.@id` == `org.@id`) et sous base_url ;
|
||
**(d)** `addressCountry` **uniforme** == `country_code` du spec (géo-ciblage
|
||
appliqué à l'identique à chaque nœud) ; **(e)** le spec **déclare** l'ancrage
|
||
(`organization.name` == marque + `source` cite CLAUDE.md). Renommer la marque dans
|
||
CLAUDE.md seul mord **simultanément** l'artefact **et** le spec — l'ancre est vive.
|
||
|
||
**Entités porteuses du DocType CONFOTUR ancrées sur CLAUDE.md §Entités.** Le champ
|
||
Select `entite_porteuse` du DocType `CONFOTUR Application` — le menu déroulant qui
|
||
fixe **quelle entité juridique** porte chaque dossier d'incitation — offre pour
|
||
options les **7 entités canoniques** du mandat (`WAF` · `WA SRL` · `AC Arias
|
||
Cuevas` · `Consortium ECR DR` · `Helios RD` · `Ploutos` · `9060 QC`). Ces options
|
||
sont **dérivées** via `options_source` de `confotur_spec.json[entites]` et
|
||
byte-gatées pour la **reproductibilité** par `check_artifacts` (l'artefact se
|
||
reconstruit depuis le spec) — mais **jamais ancrées** à CLAUDE.md §Entités, leur
|
||
source faisant autorité. Les blocs CONFOTUR amont gatent le **compte** de
|
||
champs/rôles et la cross-cohérence des **permissions**, aveugles à l'identité de
|
||
cette liste. Piège #6 : **renommer** une entité dans CLAUDE.md §Entités
|
||
(`9060 QC`→`9061 QC`) ou dans `confotur_spec.json[entites]` puis régénérer fait
|
||
offrir au DocType un menu qui **contredit/omet** une entité canonique pendant que
|
||
`check_artifacts` reste **vert** (l'artefact se reconstruit… depuis un spec dérivé)
|
||
— même « vert trompeur » que la marque SEO org (§Entités) et les tokens branding
|
||
(#4). On **re-dérive** les 7 entités des tokens en gras de la section « ## Entités »
|
||
de CLAUDE.md, puis on exige : **(a)** options du Select de l'artefact == entités
|
||
CLAUDE.md (**ordre exact**) ; **(b)** `spec[entites]` == CLAUDE.md (l'**input**
|
||
ancré) ; **(c)** le spec **déclare** l'ancrage (`entites_source` cite CLAUDE.md +
|
||
chaque entité) ; **(d)** wiring intact (`options_source == "entites"` · le déroulant
|
||
**dérive** de la liste, pas d'un dur) ; **(e)** le README cite l'ancre CLAUDE.md et
|
||
nomme chaque entité (nom complet **ou** 1er token — l'abréviation « AC » de « AC
|
||
Arias Cuevas » est un choix éditorial toléré · #6). Renommer une entité dans
|
||
CLAUDE.md seul mord **simultanément** l'artefact, le spec, sa déclaration **et** le
|
||
README — l'ancre est vive. **2ᵉ surface prose (fiche ONAPI/Legal)** : la fiche
|
||
`03_agents/onapi_legal/AGENT.md:54` ÉNUMÈRE à la main « les **7 entités** de
|
||
CLAUDE.md #Entités (…) » — un COMPTE littéral + la liste complète que le motif (e),
|
||
ancré sur le README `legal/confotur`, ne balaie pas. On re-dérive la liste de
|
||
CLAUDE.md §Entités et on exige de la fiche : **(a)** le compte littéral == |§Entités|
|
||
· **(b)** l'énumération == §Entités **ordre exact** (absent ET fantôme mordus par
|
||
set-diff) · **(c)** l'ancre « CLAUDE.md #Entités » citée · **(d)** cross-check —
|
||
l'énumération == options `entite_porteuse` de l'artefact byte-gaté (la fiche décrit
|
||
**exactement** le menu que le DocType offrira).
|
||
|
||
**Champs `dossier_vente`/`estado` du DocType CONFOTUR dérivés du workflow vente.**
|
||
La §« Cœur du livrable : cross-cohérence » du même README `legal/confotur` affirme
|
||
que **deux** champs du DocType sont **dérivés** du pipeline vente, jamais réécrits
|
||
en dur : `dossier_vente` (Link) → cible `workflow.document_type` (= `OTO Dossier
|
||
Vente`) et `estado` (Select) → options = les états `confotur_*` du **workflow
|
||
vente**. Source faisant autorité : `crm/workflow_vente/workflow_vente_spec.json` —
|
||
l'**input** que le builder confotur lit (déclaré `workflow_source` dans le
|
||
MANIFEST), d'où il tire `document_type` et les `update_value` d'états commençant par
|
||
`confotur`. Les blocs CONFOTUR amont gatent le **compte** de champs/rôles, la
|
||
cross-cohérence des **permissions** et l'ancrage des **entités** (`entite_porteuse`)
|
||
— mais **aucun** ne touche `dossier_vente`/`estado`. Piège #6 : **renommer** le
|
||
DocType porteur côté workflow (`OTO Dossier Vente`→`OTO Dossier de Vente`) ou
|
||
**ajouter/renommer** un état CONFOTUR régénère l'artefact confotur (`check_artifacts`
|
||
reste **vert** — il se reconstruit depuis le spec workflow) **mais** laisse la
|
||
**prose** README « (= `OTO Dossier Vente`) » périmée **en silence** → l'agent
|
||
ONAPI/Legal câblerait une cible de Link / un menu d'états que le pipeline ne porte
|
||
plus. Même classe que l'ancrage `entite_porteuse` ci-dessus. GATE : **(0)**
|
||
traçabilité — le MANIFEST `workflow_source` cite bien le spec workflow ; **(a)**
|
||
cross-artefact — `dossier_vente.options` (Link) == workflow `document_type` ;
|
||
**(b)** prose — la valeur README « (= `…`) » == workflow `document_type` ; **(c)**
|
||
cross-artefact — `estado.options` == `update_value` des états `confotur_*` du
|
||
workflow (**ordre** du pipeline) ; **(c′)** toute option `estado` porte le préfixe
|
||
`confotur` ; **(d)** prose — le bullet `estado` **déclare** la dérivation
|
||
(`confotur_*` + workflow vente). Un claim absent échoue **aussi** (#6). Renommer le
|
||
`document_type` côté spec seul mord **simultanément** l'artefact **et** la prose.
|
||
|
||
**3ᵉ surface du même README SEO** : la table « Livrables » décrit **en prose** la
|
||
carte hreflang (« carte `alternate` **FR/EN/ES** + `x-default` par page »). Les
|
||
deux blocs SEO précédents (comptes de mots-clés · graphe schema.org) sont
|
||
**aveugles** à ces codes de langue, qui vivent dans un **troisième** artefact
|
||
(`seo/out/seo_hreflang.json`, byte-gaté par `check_artifacts`) : **ajouter** une
|
||
langue au bundle (`pt` → 4 langues de contenu), **retirer** ou **échanger** `es`
|
||
laisse la prose figée pendant que la carte dit autre chose. On recompute
|
||
l'**ensemble** des langues de contenu (`alternate` hors `x-default`) et on exige
|
||
que la cellule les nomme **exactement** (set-diff : absent ET en trop) + mentionne
|
||
`x-default`. En bonus, quatre cohérences croisées de l'artefact : la carte est
|
||
**uniforme** (toutes les pages portent le même jeu d'`alternate`), `x-default` est
|
||
présent sur **chaque** page, la langue par défaut (`x_default_lang`) ∈ langues de
|
||
contenu (ni vacante ni étrangère), et pour chaque page `x-default`.href ==
|
||
`canonical` == href de la langue par défaut (l'invariant « x-default = langue par
|
||
défaut = canonical » du README). Un claim supprimé (« carte INTROUVABLE ») échoue
|
||
**aussi**.
|
||
|
||
**Fiche agent SEO — 2 métriques data-derived** (`03_agents/seo/AGENT.md`) : le
|
||
tableau « Livrable réellement produit » de la fiche restitue, **à côté** du compte
|
||
de mots-clés (lui déjà gaté sur README **et** fiche), deux sorties byte-gatées mais
|
||
**aveuglées côté fiche** — les blocs SEO schema.org / hreflang précédents ne lisent
|
||
que le **README** (`seo/README.md`), jamais la fiche. L25 « graphe JSON-LD :
|
||
**10 nœuds** (`Organization` + 9 `Residence`) » et L26 « **10 pages** × FR/EN/ES +
|
||
`x-default` » sont recomputées des **mêmes** artefacts que les blocs README
|
||
(`seo_schema_org.json`/`MANIFEST.json` et `seo_hreflang.json`, byte-gatés par
|
||
`check_artifacts`) : L25 — total nœuds == `schema_org_nodes`, racine == le `@type`
|
||
**singleton** du graphe, M == `listings`, type listing == le `@type` **uniforme**
|
||
des listings, plus la cohérence arithmétique `total == 1 + M` ; L26 — nombre de
|
||
pages == `|pages|` de la carte + jeu de langues de contenu == celui de l'artefact
|
||
(set-diff : absent ET en trop). **Renommer** le `@type` des listings,
|
||
**ajouter/retirer** un projet (→ N nœuds & N pages dérivent) ou **échanger** une
|
||
langue de contenu périme la fiche pendant que les trois artefacts disent autre
|
||
chose — même classe que la cellule **chat_otoia** de la fiche Frontend Console
|
||
(fiche-agent qui restitue des attributs data-derived gatés seulement en **agrégat**,
|
||
mémoire `agent-fiche-role-attrs-ungated`). Un claim supprimé échoue **aussi**.
|
||
|
||
**Catalogue de projets ancré sur CLAUDE.md** (2ᵉ surface du README
|
||
`crm/dossier_vente`) : le champ `Select` `projet` du DocType porteur
|
||
(`out/doctype_oto_dossier_vente.json`, byte-gaté par `check_artifacts`) énumère les
|
||
**9 projets OTO** (`P01 Structure` … `P09 1069 Crisfer`) que le README résume
|
||
« Projet (`Select` **P01..P09** ancré sur CLAUDE.md) ». Ce catalogue est une
|
||
**transcription** : la section « ## Projets » de **CLAUDE.md** est la source
|
||
faisant autorité (#6/#7), mais `check_artifacts` ne prouve que _DocType == spec_
|
||
(byte-for-byte) — la spec recopie elle-même la liste, donc toute la chaîne peut
|
||
**dériver** de CLAUDE.md en restant byte-verte : **renommer** un projet
|
||
(`P03 Nakua` → `P03 Nakua Bay`), en **ajouter** (`P10`) ou en **retirer** un laisse
|
||
le Select périmé tandis que la prose « ancré sur CLAUDE.md » devient fausse. Même
|
||
esprit que le **roadmap-anchor** (INV11 · `roadmap_line` ancré au fichier roadmap) :
|
||
on recompute le catalogue depuis « ## Projets » de CLAUDE.md et on exige que les
|
||
options du Select l'égalent **exactement** (set-diff absent ET en trop, **puis
|
||
ordre**), plus la plage `P01..P<dernier>` + le mot d'ancrage dans le README. En
|
||
bonus, le catalogue CLAUDE.md est vérifié **non vide, sans doublon et contigu**
|
||
depuis `P01` (byte-déterminisme du contrat). Un claim supprimé échoue **aussi**.
|
||
|
||
Même classe enfin sur le **pipeline vente CRM** : le couple « **9 états · 11
|
||
transitions** » (titre de section du README du module `crm/workflow_vente`) et le
|
||
**même** compte d'états « les **9 états** du pipeline » (README du DocType porteur
|
||
`crm/dossier_vente`, dérivé du **même** graphe) étaient transcrits **à la main**
|
||
dans **deux** docs sans aucun gate. Les deux nombres sont recomputés depuis
|
||
`crm/workflow_vente/out/MANIFEST.json` (`counts.states` + `counts.transitions`,
|
||
recomptés du `workflow_vente_spec.json` à chaque build, byte-gaté par
|
||
`check_artifacts`). Recomputer **transitions** en plus de **states** mord une
|
||
dérive du 2ᵉ nombre là où le compte d'états seul (repris dans `dossier_vente`)
|
||
resterait aveugle — ajouter un état/une transition ferait sinon dériver **les
|
||
deux** docs en silence. **2ᵉ surface du même README** : la phrase « **séparation
|
||
des pouvoirs** » énumère **à la main** les transitions quatre-yeux (« confirmer
|
||
réservation, signer contrat, approuver CONFOTUR, annuler/résilier — … `allow_self_approval
|
||
= 0` »). Le bloc de compte est **aveugle** à leur identité : **déclasser** une
|
||
transition sensible (`allow_self_approval` 0→1 dans le contrat) laisse le compte à
|
||
9/11 tandis que l'artefact la retire de l'ensemble ; **promouvoir** une transition
|
||
(ex. `Déposer CONFOTUR` → self=0) l'y ajoute sans que le compte bouge et la prose
|
||
l'**omet** ; un **échange** d'action passe aussi. On recompute l'**ensemble** des
|
||
actions quatre-yeux depuis `crm/workflow_vente/out/workflow.json` (`{action |
|
||
allow_self_approval == 0}`, byte-gaté) et on exige que la phrase l'énumère
|
||
**exactement** (absent **et** en trop mordus par set-diff ; casse ignorée + suffixe
|
||
« (perdu) » élidé — les deux transitions vers l'état *Perdu*, `Annuler`/`Résilier`,
|
||
que la prose groupe « annuler/résilier »). Cohérence croisée : `0 < |quatre-yeux| <
|
||
|transitions|` (l'ensemble sensible est non vide **et** strict). 4 morsures
|
||
vérifiées (déclassement `Signer contrat` capté là où le compte reste 9/11 ·
|
||
promotion `Déposer CONFOTUR` captée · échange prose `signer contrat`→`signer bail`
|
||
capté · énumération supprimée = INTROUVABLE ; restauré = green).
|
||
|
||
**3ᵉ surface du même README** — le CŒUR sécurité du workflow : la table « Rôles =
|
||
contrat RBAC » (l.55-62) nomme, **par pas**, le **rôle RBAC qui garde** chaque
|
||
transition. Les deux blocs ci-dessus gatent le COMPTE et la séparation des pouvoirs
|
||
mais restent **aveugles à l'identité** de ces gardes — data-derived de deux
|
||
artefacts byte-gatés : `workflow.json` (chaque transition porte `allowed`) et
|
||
`MANIFEST.roles_rbac_utilises[].erpnext_role_name` (recomputé du contrat
|
||
`rbac_50_roles.json`). **Réaffecter** un pas monétaire (« Confirmer réservation »
|
||
`Réservations`→`Conseiller` : élévation de privilège), **renommer** un rôle,
|
||
**ajouter** un rôle fantôme ou en **oublier** un laisse la table périmée pendant que
|
||
l'artefact dit autre chose → l'agent ERPNext Backend câblerait le mauvais garde. On
|
||
exige : (1) **identité d'ensemble** — colonne « Rôle » == `{allowed}` de
|
||
`workflow.json` (absent **et** en trop) ; (2) **cohérence artefacts** — `{allowed}`
|
||
== `roles_rbac_utilises` (aucun garde hors manifeste, ni l'inverse) · ensemble non
|
||
vide ; (3) **cross-cohérence par pas** — les gardes des étapes SENSIBLES et uniques
|
||
`Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` (recomputés `allowed`,
|
||
jamais figés) doivent être nommés EXACTEMENT dans leur ligne — ce qui mord la
|
||
**réaffectation vers un rôle déjà présent** que le seul set-diff manquerait. 7
|
||
morsures vérifiées (réaffectation prose `Confirmer réservation` · réaffectation
|
||
artefact `Signer contrat` · rôle fantôme · rôle manquant · incohérence MANIFEST ·
|
||
table supprimée = INTROUVABLE · réaffectation artefact `Approuver CONFOTUR` vers un
|
||
rôle déjà présent — set inchangé, captée par la cross-cohérence par pas ; restauré =
|
||
green).
|
||
|
||
**IDENTITÉ du DocType porteur** — le **NOM** `OTO Dossier Vente` : le module
|
||
`crm/dossier_vente` émet `out/doctype_oto_dossier_vente.json` dont le champ `name`
|
||
**est** le nom du DocType (= `MANIFEST.doctype_name`, byte-gaté par
|
||
`check_artifacts`). Ce nom est le **point d'attache** du pipeline : le Workflow
|
||
vente s'y branche via son `document_type`
|
||
(`crm/workflow_vente/out/workflow.json[0].document_type`). Il est **recopié à la
|
||
main** à plusieurs endroits, dont **aucun** n'était gaté sur le NOM (le bloc
|
||
pipeline ci-dessus ne recompute que le **compte** d'états, aveugle à l'identité) :
|
||
le **titre** du README `dossier_vente` (« # DocType porteur · `OTO Dossier Vente` »)
|
||
et sa ligne « **Nom du DocType** = `document_type` du workflow (`OTO Dossier
|
||
Vente`) », la fiche `03_agents/crm/AGENT.md:25` (« **DocType porteur** `OTO Dossier
|
||
Vente` ») et la fiche `03_agents/onapi_legal/AGENT.md:50` (« cible =
|
||
`workflow.document_type` (`OTO Dossier Vente`) »). Piège : **renommer** le DocType
|
||
dans le spec reconstruit les **deux** artefacts de façon cohérente (le
|
||
`document_type` du workflow suit, les tests — qui testent des FONCTIONS — restent
|
||
verts) tandis que les mentions en prose se **périment en silence** → l'agent
|
||
ERPNext Backend importerait un DocType sous un nom pendant que la fiche/le README
|
||
en nomment un autre, et le Workflow s'attacherait à un DocType **fantôme** — la
|
||
rupture même que « sans lui le pipeline n'a rien à quoi s'attacher » veut
|
||
prévenir. On **recompute** le nom depuis l'artefact (zéro duplication) et on exige
|
||
que chaque prose le nomme **exactement** (backtické). Cohérences croisées :
|
||
`DocType.name == MANIFEST.doctype_name` (cohérence interne du module) **et**
|
||
`== workflow.document_type` (l'attache porteur↔workflow — sinon le Workflow vise un
|
||
DocType fantôme). 7 morsures vérifiées (nom artefact renommé → mord les 6 lignes en
|
||
cascade · `document_type` du workflow divergent → attache rompue · `doctype_name`
|
||
du MANIFEST divergent → incohérence interne · fiche CRM:25 périmée · fiche
|
||
onapi_legal:50 périmée · ligne « Nom du DocType » périmée · titre du README périmé ;
|
||
un claim absent échoue **aussi** ; restauré = green).
|
||
|
||
Même classe encore sur l'**app mobile Expo** : la ligne récap du README du module
|
||
`mobile/app_config` (« **5 onglets** · **44 rôles** couverts · **3 langues** ·
|
||
**13 identifiants `a_confirmer`** ») transcrivait **à la main** un quadruplet dont
|
||
chaque nombre est recompté à chaque build depuis `rbac_50_roles.json` (portails
|
||
métier + rôles), `seo_spec.json` (langues) et la liste `a_confirmer` (credentials
|
||
EAS/stores jamais fabriqués). Les quatre sont recomputés depuis
|
||
`mobile/app_config/out/MANIFEST.json` (`counts.{onglets,roles_couverts,langues,
|
||
identifiants_a_confirmer}`, byte-gaté par `check_artifacts`). Recomputer **les
|
||
quatre** volets mord une dérive de n'importe lequel (ajouter un rôle/portail RBAC,
|
||
une langue, un credential store) là où un seul nombre resterait aveugle. **2ᵉ
|
||
surface du même README** : la même ligne récap **énumère** les 5 onglets
|
||
(« (Ventes/Construction/Achat/Compta/Direction) ») — le patron du compte matche
|
||
la parenthèse par `\([^)]*\)` mais en **ignore le contenu**. Cette liste est
|
||
data-derived : `mobile/app_config/out/role_navigation.json` (byte-gaté) porte un
|
||
onglet par portail métier (bijection 1 onglet ⇔ 1 portail), chaque entrée = son
|
||
`label` « OTO <Nom> ». On recompute la **liste ordonnée** des noms courts et on
|
||
exige que la parenthèse l'énumère **exactement** : un **échange** (Ventes→
|
||
Marketing), un **retrait** (Compta absent — l'énumération tombe à 4 tandis que
|
||
« **5 onglets** » reste juste) ou un **réordre** est mordu là où le seul compte
|
||
est aveugle. Cohérence croisée : `len(role_navigation) == counts.onglets ==
|
||
counts.portails` — mord un compteur du manifeste désynchronisé de sa navigation.
|
||
|
||
**Non-surface documentée — `roles_couverts` (« 44 ») N'EST PAS cross-gaté, à dessein.**
|
||
Le total « 44 rôles couverts par les 5 portails » apparaît, identique, dans **trois**
|
||
manifestes (`frontend/portails`, `frontend/chat_otoia`, `mobile/app_config` ·
|
||
`counts.roles_couverts=44`). On pourrait croire à trois rendus indépendants réclamant
|
||
un cross-check façon RBAC « 50 » (où `rbac/apply_plan/rbac_apply_plan.py:149` exige
|
||
`fixtures==userperm==roleprofile`, car chacun **re-dérive** son décompte avec sa propre
|
||
logique). Ce n'est **pas** le cas ici : les trois modules **importent la même** fonction
|
||
de partition — `mobilelib/deps.py:77` et `chatlib/deps.py:84` font tous deux
|
||
`roles_by_portail = portails_builder.roles_by_portail` (l'unique définition,
|
||
`frontend/portails/wslib/builder.py:36`) — appliquée à l'**unique** contrat
|
||
`rbac/rbac_50_roles.json`. `roles_couverts` est donc **transitivement mono-sourcé**
|
||
(code partagé + donnée partagée) : les trois totaux ne peuvent diverger que si cette
|
||
fonction ou ce contrat change, auquel cas les trois changent ensemble et
|
||
`check_artifacts` (reproductibilité byte-à-byte) vérifie chaque rebuild. Un gate
|
||
« 44==44==44 » serait **redondant** (#5) — cf. mémoire *fix-vs-gate-transitively-
|
||
protected-constant*. Chaque « 44 » vif reste néanmoins ancré à SON manifeste
|
||
(mobile README ci-dessus · `demo/scenarios/out/run_sheet.md` sourcé à
|
||
`portails/…/roles_couverts`), ce qui suffit : la valeur ne peut mentir que si son
|
||
manifeste ment, et le manifeste est byte-gaté.
|
||
|
||
Même classe enfin sur le **DocType CONFOTUR Application** (livrable `legal/confotur`) :
|
||
la ligne de synthèse du README du module (« **18 champs** (14 de donnée) · **4
|
||
sections** · **3 rôles** · … · **2 évènements de dépôt** ») **et** la cellule de la
|
||
fiche agent `onapi_legal` transcrivaient **à la main** le même quintuplet, dérivé du
|
||
spec du DocType. Les cinq nombres sont recomputés depuis
|
||
`legal/confotur/out/MANIFEST.json` (`counts.{fields,data_fields,sections,roles,
|
||
depot_events}`, recomptés du `confotur_spec.json` à chaque build, byte-gaté par
|
||
`check_artifacts`) et comparés aux **deux** docs. Recomputer **les cinq** volets mord
|
||
une dérive de n'importe lequel (ajouter un champ/une section/un rôle au spec) là où un
|
||
seul nombre resterait aveugle. **2ᵉ surface du même README CONFOTUR** — le CŒUR
|
||
sécurité du livrable : la section « Cross-cohérence » §Permissions énumère **à la
|
||
main**, PAR rôle, son **portail** ET son **jeu d'actions** RBAC (annoncé « mot pour
|
||
mot les `permissions_cibles` … ni ajout ni retrait ») — `ventes-confotur` (Ventes)
|
||
→ read/write/create/print · `legal-onapi` (Direction) → read/write/create ·
|
||
`legal-directeur` (Direction) → read/write/**submit**/report. Le compte « 3 rôles »
|
||
est **aveugle** à leur identité : **promouvoir** `ventes-confotur` à `submit`
|
||
(élévation de privilège cassant la séparation des pouvoirs dont `is_submittable` est
|
||
déduit), **retirer** une action, **réaffecter** un portail, **renommer** un rôle ou
|
||
**ajouter** une ligne fantôme laisse la prose périmée pendant que les artefacts
|
||
byte-gatés (`out/MANIFEST.json.roles_rbac_utilises[]` + `out/doctype_confotur_
|
||
application.json.permissions[]`) disent autre chose → l'agent ERPNext Backend
|
||
câblerait le mauvais jeu de permissions (le risque même que la cross-cohérence veut
|
||
prévenir), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de mapping RBAC,
|
||
pas la prose) n'attrape. On RECOMPUTE le jeu d'actions + le portail de chaque rôle
|
||
depuis le MANIFEST et on exige que la prose l'encode **exactement** (set-diff :
|
||
absent **et** en trop · casse normalisée), plus l'**identité d'ensemble** (README ⇔
|
||
MANIFEST — aucun rôle fantôme ni manquant). Cohérences croisées en bonus (mordent un
|
||
artefact **internement incohérent**) : MANIFEST ⇄ DocType d'accord sur le jeu
|
||
d'actions par rôle · **séparation des pouvoirs** — `submit` porté par **exactement
|
||
un** rôle (`legal-directeur`, comme le singleton `set_user_permissions` du catalogue
|
||
RBAC) et `is_submittable == True` déduit de sa présence (README:31). 7 morsures
|
||
vérifiées : README promeut `ventes-confotur` à `submit` (en trop) · README réaffecte
|
||
`legal-onapi` Direction→Ventes · README retire `report` de `legal-directeur`
|
||
(sous-ensemble) · README renomme un `role_id` (fantôme + manquant) · énumération
|
||
supprimée (INTROUVABLE) · MANIFEST promeut `ventes-confotur` à `submit` (README
|
||
périmé + séparation cassée + incohérence MANIFEST⇄DocType) · DocType
|
||
`is_submittable=false` alors qu'un rôle porte `submit` (incohérence interne) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur la **synthèse de l'audit 5D** (livrable `qa/audit_5d`) : la
|
||
phrase d'ouverture du README du module (« **17 contrôles** répartis en **5 dimensions
|
||
(5D)** ») **et** la cellule de la fiche agent `qa` (« 17 contrôles / 5 dimensions »)
|
||
transcrivaient **à la main** le même couple, dérivé du catalogue de contrôles. Ce
|
||
couple est recomputé depuis `qa/audit_5d/out/MANIFEST.json` (`controls_total` +
|
||
`len(dimensions)`, byte-gaté par `check_artifacts`) et comparé aux **deux** docs. La
|
||
**ventilation du verdict** portée par le seul README (« **13 PASS · 0 FAIL · 4 à
|
||
confirmer** ») est recomputée à part depuis `qa/audit_5d/out/audit_report.json`
|
||
(`totals.{pass,fail,a_confirmer}`, aussi byte-gaté) : elle mord une **bascule** d'un
|
||
contrôle en A_CONFIRMER (pass 13→12, à confirmer 4→5) là où le total de contrôles
|
||
reste 17 et resterait aveugle. Cohérence croisée en bonus : `controls_total` du
|
||
MANIFEST doit égaler `totals.controls` du rapport.
|
||
|
||
Deuxième surface du **même README** de l'audit 5D : la **table « Verdict courant »**
|
||
énumère **à la main** les 4 open items (`| D1.1 · … | D1 | Direction |` …), chacun
|
||
portant trois faits data-derived — l'`id` du contrôle, sa **dimension** et son
|
||
**propriétaire**. Le bloc de ventilation ci-dessus ne gate que le **compte** (« 4 à
|
||
confirmer ») : un **échange** d'open item (ex. `D2.3 → D3.1`), une **dimension** mal
|
||
étiquetée (`D1.3` rangé sous `D2`) ou un **propriétaire** réattribué laisserait le
|
||
compte à 4 et **passerait en silence**. On RECOMPUTE l'ensemble
|
||
`{control:(dimension, propriétaire)}` depuis `qa/audit_5d/out/audit_report.json`
|
||
(`open_items[]`, byte-gaté) et on exige que la table README l'énumère **exactement** :
|
||
contrôle par **ensemble** (absent **et** en trop mordus, pas seulement présence),
|
||
puis `(dimension, propriétaire)` **par ligne** — même patron que l'énumération des
|
||
confirmations DevOps ou la carte de renormalisation par archétype 4Big. Cohérence
|
||
croisée en bonus : l'ensemble des `control` == `MANIFEST.open_items` (le manifeste qui
|
||
résume le rapport) — mord un manifeste désynchronisé de son propre rapport.
|
||
|
||
Troisième surface du **même README** de l'audit 5D : la **table « ## Les 5
|
||
dimensions »** énumère **à la main**, par ligne, le triplet (`id`, `titre`,
|
||
`reference`) de chaque dimension du cadre (`| **D1** | Traçabilité & anti-invention |
|
||
ISA 500 — Éléments probants |` …). Le tout **premier** bloc audit 5D ne gate que le
|
||
**compte** (« 5 dimensions ») — un **wildcard aveugle à QUELLE dimension** :
|
||
**renommer** un libellé (« Traçabilité & anti-invention » → …) ou **permuter une
|
||
norme de référence** (D3 fiscal citant la mauvaise `Ley 32-23`, D1 probatoire citant
|
||
`ISA 501` au lieu d'`ISA 500`) laisserait le compte à 5 et **passerait en silence** —
|
||
alors que c'est l'audit de **second niveau** que l'auditeur/banquier lit : une norme
|
||
de conformité périmée est une **affirmation fausse** (l'invention même que #6
|
||
interdit). On RECOMPUTE la liste **ordonnée** `{id:(titre, reference)}` depuis
|
||
`qa/audit_5d/out/audit_report.json` (`dimensions[]`, byte-gaté) et on exige que la
|
||
table README l'énumère **exactement** : `id` par **ensemble** (absent **et** en trop
|
||
mordus), **ordre** préservé (D1→D5), puis `(titre, reference)` **par ligne**.
|
||
Cohérence croisée en bonus : les dimensions du rapport == celles de `audit_spec.json`
|
||
(l'INPUT dont le rapport dérive) — mord un builder qui renommerait/permuterait une
|
||
dimension en aval du spec. Table disparue = échec (traçabilité #6).
|
||
|
||
**4ᵉ surface du même README** de l'audit 5D : la section **« ## Livrables audités
|
||
(source = leur `out/`) »** énumère **à la main** les **5 hand-off `out/*.json`** que
|
||
l'audit de **second niveau** LIT comme matière première (`crm/workflow_vente/out/
|
||
workflow.json` … `legal/confotur/out/doctype_confotur_application.json`). Les trois
|
||
blocs audit 5D ci-dessus gatent la **synthèse** (contrôles/dimensions/verdict),
|
||
l'énumération des **open items** et l'identité des **5 dimensions** — **aucun** ne
|
||
touche la liste des **artefacts sources**. Cette liste vit dans
|
||
`qa/audit_5d/out/MANIFEST.json` (`audited_artifacts`, byte-gaté par `check_artifacts`
|
||
⇒ faisant autorité : le générateur la recompute à chaque build via
|
||
`sorted(ARTIFACT_PATHS.values())`, le registre des hand-off amont que
|
||
`load_artifacts()` charge réellement). Le README réénumère ces 5 chemins **dans un
|
||
ordre différent** du MANIFEST (workflow 2ᵉ ici, 3ᵉ au MANIFEST trié) ⇒ comparaison
|
||
**par ensemble**. Piège : **brancher** un nouveau livrable amont sur l'audit
|
||
(nouvelle entrée `ARTIFACT_PATHS` → nouveau chemin dans `audited_artifacts`) ou en
|
||
**retirer** un laisse la section README **périmée en silence** pendant que l'audit
|
||
lit un autre jeu de fichiers — le lecteur/banquier croirait l'audit couvrir un
|
||
périmètre qu'il ne couvre plus (l'invention #6 interdit), qu'aucune suite `tests/`
|
||
(qui teste des FONCTIONS de contrôle, pas la prose) n'attrape. On RECOMPUTE
|
||
l'ensemble depuis `audited_artifacts` et on exige que les **puces** README l'énumèrent
|
||
**exactement** (absent **et** fantôme mordus). Cohérences croisées bonus : **(a)**
|
||
chaque chemin audité **résout** un fichier commité sur disque (le `_load` de l'auditeur
|
||
lève `FileNotFoundError` sinon — audit à vide impossible) ; **(b)** le **nombre** de
|
||
livrables audités == le nombre d'**alias distincts** d'artefacts référencés par les
|
||
`controls[].artifacts` du `audit_spec.json` (un artefact chargé mais jamais contrôlé,
|
||
ou un alias contrôlé sans chemin de chargement → `KeyError` runtime, désynchronise les
|
||
deux). Section absente = échec (traçabilité #6).
|
||
|
||
Même classe enfin sur le **run-book de déploiement VPS** (livrable
|
||
`devops/deploy_runbook`) : le compte de **phases** (fiche agent `devops` — « graphe
|
||
de **7 phases ordonnées** » **et** « exécute les 7 phases sur le VPS ») **et** le
|
||
compte de **confirmations préalables** (README du module — « renseigner les 7
|
||
confirmations ») transcrivaient **à la main** deux nombres qui, aujourd'hui, valent
|
||
**tous deux 7** — un gate qui n'en recompterait qu'un serait **aveugle** à la dérive
|
||
de l'autre. Chacun est recomputé depuis **son propre** champ de
|
||
`devops/deploy_runbook/out/MANIFEST.json` (`counts.phases` = `len(graph.phase_order)` ·
|
||
`counts.confirmations` = `len(confirmations_ouvertes)`, byte-gaté par
|
||
`check_artifacts`) et comparé aux docs. Bonus anti-dérive : l'**énumération** des
|
||
confirmations du README (les tokens `snake_case`) doit être l'ensemble **exact** de
|
||
`confirmations_ouvertes` — mord l'**échange** d'un nom de confirmation là où le seul
|
||
compte (reste 7) resterait aveugle.
|
||
|
||
**2ᵉ surface `devops/deploy_runbook`** : la **table « Plan de phases généré »** du
|
||
MÊME README (« # | Responsable | Phase | Modules | Dépend de ») porte, par phase,
|
||
quatre données data-derived de `out/deploy_runbook.json` (byte-gaté) — le n° d'ordre
|
||
(`#`), le `Responsable` (worker/vps/worker+vps), l'ensemble des **Modules** gated
|
||
déployés dans la phase, et « Dépend de » (les n°s d'ordre des `depends_on`) — que le
|
||
bloc de comptes ci-dessus ne couvrait pas (colonnes **wildcard**). Défaut réel
|
||
reproduit : la table **omettait** `mobile/app_config` (phase 5) **et** `qa/acceptance`
|
||
(phase 7) — **20 modules listés sur 22** — pendant que le MANIFEST prouvait la
|
||
couverture **bijective 22/22 vs CI** : vert trompeur, l'agent DevOps aurait sauté 2
|
||
modules au portage. Le gate recompute `#`/`Responsable`/`Modules`/`Dépend de` depuis
|
||
`deploy_runbook.json` et exige que la table les énonce **exactement** par phase ;
|
||
identité d'ensemble : {ordres des lignes} == {ordres de l'artefact} (aucune phase
|
||
fantôme/manquante) **et** l'**union** des cellules Modules == l'ensemble des modules de
|
||
l'artefact (aucun module oublié/en trop). La colonne « Phase » reste libre
|
||
(paraphrase). Cohérences croisées en bonus (mordent un plan interne incohérent) :
|
||
ordres contigus `1..N` sans doublon · `responsable` ∈ {worker, vps, worker+vps} ·
|
||
toute dépendance pointe en **arrière** (n° < n° de la phase ⇒ pas de cycle).
|
||
|
||
**3ᵉ surface `devops/deploy_runbook` — imputabilité des confirmations** : la ligne
|
||
« Confirmations préalables VPS » du README annote chaque confirmation réglementaire
|
||
d'un **couple** data-derived `nom` (**owner** · **réf-contrôle**), ex.
|
||
`taux_commission` (Direction · audit_5d D1.1). L'**owner** = qui doit confirmer le
|
||
paramètre avant déploiement ; la **réf** = le contrôle `qa/audit_5d` (`D#.#`) — ou la
|
||
session — qui en assure le suivi. Le bloc de comptes ci-dessus ne gate que le
|
||
**nombre** de confirmations et l'**ensemble de leurs noms** — aveugle à **qui** porte
|
||
chaque confirmation et à **quel contrôle** la trace. On recompute owner + réf des
|
||
`confirmations` de `out/deploy_runbook.json` (byte-gaté) **croisées** avec le rapport
|
||
`qa/audit_5d/out/audit_report.json` (byte-gaté), dont chaque contrôle porte son propre
|
||
`owner`. Cross-anchor à trois : owner run-book **compatible** avec l'owner du contrôle
|
||
audit_5d cité (sous-ensemble de tokens · tolère le suffixe éditorial « / UAF ») ·
|
||
owner README ⊆ owner run-book (tolère l'abréviation « Fiscaliste eCF » ⊂ « Compta
|
||
Fiscaliste eCF ») · réf README == contrôle réel (présent **dans audit_5d ET dans la
|
||
source**). Réaffecter `taux_commission` de Direction à Ventes (mauvais responsable
|
||
escaladé), misattribuer le contrôle (`D1.1`→`D2.1`) ou citer un contrôle **fantôme**
|
||
(`D9.9`, absent d'audit_5d) mord — « vert trompeur » qu'aucune suite `tests/`
|
||
(FONCTIONS de graphe) n'attrape. Une confirmation tracée par un contrôle **doit
|
||
rester annotée** (disparition = régression de traçabilité #6) ; une annotation d'un
|
||
nom **fantôme** échoue aussi.
|
||
|
||
Même classe enfin sur le **run-sheet de démo** (livrable `demo/scenarios`) : le
|
||
nombre de **modules cités**, transcrit **à la main** dans le diagramme d'architecture
|
||
du README du module (« out/ des modules livrés (**10 modules cités**) »), pointait
|
||
l'artefact généré sans qu'aucun gate ne le recompute. Il est recomputé depuis
|
||
`demo/scenarios/out/MANIFEST.json` (`counts.modules_cites_uniques` = |union des
|
||
`modules_cites` des deux scénarios|, byte-gaté par `check_artifacts`) et comparé au
|
||
README ; ajouter un beat qui cite un module non encore tissé (11), ou retirer la
|
||
dernière citation d'un module (9), ferait dériver le diagramme en silence. Cohérence
|
||
croisée en bonus : `counts.modules_cites_uniques` == |`modules_cites`| (liste
|
||
dédupliquée du manifeste) — mord un compteur désynchronisé de sa propre liste.
|
||
|
||
**2ᵉ surface `demo/scenarios`** : la **table des scénarios** du MÊME README
|
||
(« Scénario | Projet | Audience | Angle ») porte, dans ses trois colonnes
|
||
d'identité, des valeurs data-derived de `out/run_sheet.json` (byte-gaté) —
|
||
l'`id` (`S-P07-BANQUIER`), le couple `projet`+`projet_libelle`
|
||
(`P07 Aqua Terra Las Terrenas`) et l'`audience` (`banquier`) — que le bloc Démo
|
||
ci-dessus ne couvrait pas (il ne gate que le compte de modules). Les libellés
|
||
projets sont d'ailleurs déclarés « proviennent **verbatim** de CLAUDE.md ·
|
||
§Projets » sans aucun ancrage. Le gate (1) **ANCRE** chaque `P{code} {libellé}`
|
||
du run_sheet à l'entrée correspondante de la section « ## Projets » de `CLAUDE.md`
|
||
(verbatim, source unique) ; (2) exige que chaque **ligne** de la table porte
|
||
EXACTEMENT id + `P{code} {libellé}` + audience (fin des colonnes wildcard) ;
|
||
(3) impose l'**identité d'ensemble** — l'ensemble des ids de lignes == ids du
|
||
run_sheet == `counts.scenarios` (aucune ligne fantôme d'un scénario inventé, aucun
|
||
scénario manquant). Cohérences croisées en bonus (mordent un artefact interne
|
||
incohérent) : `run_sheet` ↔ `MANIFEST` d'accord sur `(id, projet, audience)` ·
|
||
`id == S-{projet}-{AUDIENCE}` · `counts.scenarios == |scenarios|` · ids non vides
|
||
et sans doublon. Sans ce gate, renommer un id, classer un scénario sous le mauvais
|
||
projet/audience, ajouter une ligne fantôme ou laisser un libellé projet dériver de
|
||
CLAUDE.md restait « vert » alors que le présentateur pitcherait le mauvais couple
|
||
projet/audience — le risque même que la run-sheet veut éliminer.
|
||
|
||
Même classe enfin sur le **montage Chat OTOIA par portail** (livrable
|
||
`frontend/chat_otoia`) : le nombre de **`Custom Block`**, de **configs runtime** et
|
||
de **portails** couverts, transcrits **à la main** dans DEUX docs (fiche agent
|
||
`frontend_console` — « **N `Custom Block` + M configs runtime** » · README du
|
||
module — « Portails (P, `plateforme` exclu) », « N fixtures `Custom Block` », « M
|
||
configs runtime »), pointaient l'artefact sans qu'aucun gate ne les recompute. Ces
|
||
trois nombres valent **tous 5 aujourd'hui** — un gate qui n'en recompterait qu'un
|
||
serait **aveugle** à la dérive des autres (ajouter un portail au contrat RBAC →
|
||
chat_otoia rebâtit 6 blocks ⁄ 6 mounts ⁄ 6 portails pendant que les docs restent à
|
||
5). Chacun est recomputé depuis **son propre** champ de
|
||
`frontend/chat_otoia/out/MANIFEST.json` (`counts.custom_blocks` · `counts.mounts` ·
|
||
`counts.portails`, byte-gaté par `check_artifacts`) et comparé aux docs. Cohérence
|
||
croisée : `custom_blocks` == `mounts` == `portails` == |`MANIFEST.portails`| (la
|
||
bijection 1 block ⇔ 1 mount ⇔ 1 portail du générateur) — mord un compteur
|
||
désynchronisé de sa propre liste.
|
||
|
||
Ce gate de COMPTE reste toutefois **aveugle à CE QUE dit chaque config runtime**.
|
||
La MÊME cellule « Métrique vérifiée » de la fiche `frontend_console` (`03_agents/
|
||
frontend_console/AGENT.md:29`) restitue en plus TROIS **attributs** data-derived du
|
||
montage — « persona **Amélie** … · langues **FR/EN/ES** · `` `endpoint: null` `` » —
|
||
qu'aucun gate ne recomputait sur CETTE fiche. (Le gate persona/capabilities
|
||
« ancré sur CLAUDE.md §Architecture cible » — voir plus bas — vise
|
||
le spec, les 5 mounts, le MANIFEST, le README et l'oracle du test — **jamais** cette
|
||
cellule de fiche, et il ignore langues comme endpoint.) On recompute donc les trois
|
||
depuis les artefacts byte-gatés `out/chat_mount.json` (une config par portail, 5) et
|
||
`out/MANIFEST.json` : **persona** == `MANIFEST.persona.nom` (« Amélie » ; le « QC »
|
||
est le qualificatif de marque `Voix Amélie QC` de CLAUDE.md, gardé en littéral non
|
||
gaté), **langues** `FR/EN/ES` == `MANIFEST.langues` en majuscule, et **`endpoint:
|
||
null`** — une **affirmation d'honnêteté #6/#8** : l'endpoint OTOIA n'est PAS fabriqué
|
||
(renseigné côté VPS). Le gate exige que CHAQUE mount ait `endpoint == null` ; qu'une
|
||
config gagne un endpoint réel dans l'artefact et la fiche « endpoint: null » devient
|
||
un **mensonge en silence** → morsure. Cohérences croisées (bijection du générateur) :
|
||
`MANIFEST.persona.nom` et `MANIFEST.langues` doivent valoir EXACTEMENT ce que porte
|
||
CHAQUE mount (un montage désynchronisé de son propre manifeste mord), et
|
||
`endpoint_statut == 'a_confirmer'` doit correspondre à des endpoints TOUS `null`
|
||
(sinon incohérence interne de l'artefact). Un claim absent échoue AUSSI. Même classe
|
||
de « vert trompeur » que la fiche mobile / les fiches faisabilité : une suite `tests/`
|
||
teste les FONCTIONS du builder, jamais la prose de la fiche.
|
||
|
||
Même classe enfin sur le **module de recette** (`qa/acceptance`) : le couple
|
||
« **N livrables de sprint** + **M métriques succès MVP** » est transcrit **à la
|
||
main** dans DEUX docs que le bloc racine ne couvre pas — le README du module
|
||
(deux puces) ET la fiche agent QA (« (N livrables + M métriques) »). Le bloc
|
||
racine ne gate que le SEUL README d'entrée (« N promesses (X sprint + Y
|
||
métriques) ») ; ces deux docs de module en dérivent. Ajouter une promesse roadmap
|
||
(l'artefact rebâtit 9/7) ferait dériver les deux docs en silence — piège que ni
|
||
les suites `tests/` ni le **roadmap-anchor** (qui ancre les `roadmap_line` de
|
||
l'ARTEFACT, pas la prose des docs) n'attrapent. Chaque nombre est recomputé depuis
|
||
**son propre** genre de la matrice `qa/acceptance/out/acceptance_matrix.json`
|
||
(`kind` `sprint_deliverable` / `mvp_metric`, byte-gatée par `check_artifacts`) et
|
||
comparé aux deux docs. Cohérence croisée en bonus :
|
||
`MANIFEST.counts.{sprint_deliverables,mvp_metrics}` == le compte de la matrice
|
||
qu'il résume — mord un compteur du manifeste désynchronisé de sa liste.
|
||
|
||
Même classe enfin sur le **barème de l'auditeur 4Big** (livrable `qa/audit_4big`).
|
||
Ironie déjà notée : le module qui NOTE les autres retranscrit son propre référentiel
|
||
de scoring **à la main** dans son README (`05_deliverables_mvp/qa/audit_4big/README.md`)
|
||
— la prose « Note chaque module sur **N critères 4Big déterministes** », la table
|
||
« Critère | Poids | … » (une ligne `id`→poids par critère) et l'archétype
|
||
« `generator` (**N** critères) ». Ces trois surfaces DÉRIVENT de
|
||
`quality_report.json.criteria` (byte-gaté par `check_artifacts`, recomputé du
|
||
référentiel à chaque build) ; ajouter/retirer un critère ou changer un poids ferait
|
||
dériver le README en silence pendant que l'artefact dit autre chose — dérive qu'aucune
|
||
suite `tests/` (qui teste des FONCTIONS, pas la prose) n'attrape. Le bloc racine ne
|
||
gate que la ligne « Résultat courant » (verdict/modules), jamais le barème. On
|
||
recompute donc le **nombre de critères** (prose + « generator »), et **chaque poids**
|
||
depuis `criteria[].weight` (zéro duplication du barème du générateur · #6). Cohérences
|
||
croisées : `Σ poids == 100` (base de renormalisation documentée) ET tout module
|
||
d'archétype `generator` porte EXACTEMENT `len(criteria)` `checks` — c.-à-d.
|
||
« generator = tous les critères », ce qui rend le « generator (N critères) » du README
|
||
vérifiablement vrai (mord une dérive du modèle qui le rendrait faux). Une **compensation**
|
||
(un poids +5 / un autre −5) laisserait `Σ == 100` juste et deux lignes fausses : d'où
|
||
le contrôle **par critère**, pas seulement de la somme. 4 morsures vérifiées : dérive
|
||
prose `5→6` (poids intacts) · dérive poids `TESTS 25→30` (compte intact) · dérive
|
||
`generator (5→4)` · claim prose disparu ; restauré = vert.
|
||
|
||
Une **autre surface** du même README est la carte de **renormalisation par
|
||
archétype** : « `generator` (5 critères) · `data_room` (**sans HANDOFF**) ·
|
||
`parser` (**sans CONTRAT** local ni **HANDOFF**) · `contract` (**sans CLI** ni
|
||
**HANDOFF**) ». Le bloc barème ne recomputait QUE `generator` ; les **exclusions**
|
||
des trois autres archétypes n'étaient recomputées par AUCUN gate. Or elles sont
|
||
DATA-DERIVED : pour un archétype, l'ensemble des critères **applicables** =
|
||
`{check.criterion}` de ses modules dans `quality_report.json` (les non-applicables
|
||
sont **absents** des `checks`) ⇒ **exclus** = ordre du barème − applicables. Changer
|
||
la règle de renormalisation d'un archétype (ex. `data_room` exclut aussi `CLI`)
|
||
ferait dériver cette prose en silence. On recompute donc, PAR archétype, l'ensemble
|
||
des critères exclus et on exige que la parenthèse du README l'énumère **exactement**.
|
||
Bonus d'uniformité : tous les modules d'un même archétype portent le **même** jeu
|
||
applicable (« 1 archétype → 1 jeu de critères », base même de la carte). 3 morsures
|
||
vérifiées : `data_room` « sans HANDOFF »→« sans CLI » (mauvais critère) · `parser`
|
||
exclusion `HANDOFF` retirée (sous-ensemble) · énumération `contract` supprimée ;
|
||
restauré = vert.
|
||
|
||
Une **troisième surface** du même README est la colonne **« Fait vérifié »** de la
|
||
table du barème : elle NOMME les deux **seuils structurels** que le scorer applique —
|
||
`DOC` « un `*.md` ≥ **400 octets** » et `TESTS` « ≥ **8 méthodes** `def test_*` ».
|
||
Ces deux nombres sont DATA-DERIVED de `quality_spec.json[thresholds]`
|
||
(`min_doc_bytes`/`min_test_methods`) — l'INPUT du générateur, byte-gaté par
|
||
`check_artifacts` qui prouve que `quality_report.json` se rebuild depuis lui — et
|
||
TRANSCRITS À LA MAIN. Le bloc barème ne capturait QUE le **poids** (2e cellule),
|
||
aveugle à la 3e : élever `min_test_methods` (8→12 : un module de 8-11 tests
|
||
tomberait sous `FAIL`) ou changer `min_doc_bytes` rebuild le report SANS TOUCHER au
|
||
README → prose « ≥ 8 méthodes »/« ≥ 400 octets » périmée en silence, l'auditeur
|
||
documentant un barème qu'il n'applique plus. On recompute les deux seuils depuis le
|
||
spec et on exige que les cellules DOC/TESTS les nomment EXACTEMENT ; **cross-check
|
||
d'ancrage** : l'evidence `TESTS` de CHAQUE module du report porte « (seuil N) »
|
||
(émise par `crit_tests`) — on exige `N == spec.min_test_methods` pour tous, prouvant
|
||
que le seuil ÉCRIT == le seuil réellement APPLIQUÉ (mord une divergence
|
||
spec↔report). 5 morsures vérifiées : README `DOC 400→450` · README `TESTS 8→10` ·
|
||
README ligne DOC vidée (claim absent) · spec `min_test_methods 8→12` (README périmé
|
||
+ cross-check spec↔report) · spec `min_doc_bytes 400→500` ; restauré = vert.
|
||
|
||
Même classe enfin sur le **contrat de format de l'e-NCF** (livrable
|
||
`fiscal/ecf_dgii`) : le format de l'identifiant fiscal électronique DGII
|
||
« **E + tipoeCF(2) + secuencia(10)** » (= **13**) — préfixe, longueur du segment
|
||
tipo, longueur du segment secuencia et longueur totale — est le CŒUR du module (le
|
||
« composeur traçable ») et il est retranscrit **à la main** dans **quatre** endroits
|
||
de prose : le résumé du README (`E + tipoeCF(2) + secuencia(10)`), l'invariant 7 du
|
||
README (`E+tipo(2)+seq(10)=13`, seul à porter la longueur **totale**), la section
|
||
composition du README (`e_ncf = "E" + tipo(2) + secuencia(10)`) et la fiche agent
|
||
`erpnext_backend`. Le bloc racine ne gate que le **compte de tests** e-CF (« 39 ») ;
|
||
ce format n'était recomputé par AUCUN gate. Or il est **data-derived** du bloc
|
||
`e_ncf` d'`fiscal/ecf_dgii/out/ecf_plan.json` (byte-gaté par `check_artifacts`) :
|
||
`prefix` (« E »), `longueur` (13), `regex` (« `^E[0-9]{12}$` ») et `structure`
|
||
(« E + tipoeCF (2 chiffres) + secuencia (10 chiffres) »). Allonger la secuencia (un
|
||
rango DGII à 12 chiffres → `longueur` 15) ou changer le préfixe ferait dériver les
|
||
quatre proses en silence pendant que l'artefact dit autre chose — dérive qu'aucune
|
||
suite `tests/` (qui teste les FONCTIONS `compose_encf`/`parse_encf`, pas la prose)
|
||
n'attrape. On RECOMPUTE `(prefix, tipo_len, seq_len, total)` depuis le SEUL bloc
|
||
`e_ncf` (zéro duplication du format du générateur · #6) et on exige que **chaque**
|
||
prose l'encode exactement. Cohérences croisées en bonus (mordent un artefact
|
||
**internement incohérent**) : `1 + tipo_len + seq_len == longueur` (1+2+10 == 13) ET
|
||
le nombre de chiffres du `regex` == `tipo_len + seq_len` (12 == 2+10). 5 morsures
|
||
vérifiées : drift de l'artefact source (secuencia `10→12`, les 4 proses mordues) ·
|
||
résumé README `tipo 2→3` · invariant README total `13→14` (aveugle au segment) ·
|
||
fiche `erpnext_backend` `secuencia 10→9` · composition README supprimée
|
||
(INTROUVABLE) ; plus la morsure d'incohérence d'artefact (`regex` désaligné du
|
||
segment) ; restauré = vert.
|
||
|
||
Même classe enfin sur la **cross-cohérence e-CF DGII** (2e surface du MÊME README
|
||
`fiscal/ecf_dgii`) — le CŒUR du livrable. Le bloc e-NCF ci-dessus ne gate que le
|
||
**format** de l'identifiant ; la section « Cross-cohérence e-CF ↔ workflow ↔ DocType
|
||
↔ RBAC » énumère **à la main** trois données data-derived d'`out/ecf_plan.json`
|
||
(byte-gaté par `check_artifacts`) : les **états qui émettent** un e-CF « seulement
|
||
réservation et contrat » (README:48) = `{emission_events[].update_value}`, les
|
||
**champs Currency de base** « `montant_reservation`, `montant_contrat` » (README:50)
|
||
= `{emission_events[].base_field}`, et la **FormaPago défaut** « = 3 (Tarjeta) …
|
||
via Cardnet (#10) » (README:59) = `forma_pago_defaut` **ancrée sur CLAUDE.md #10**
|
||
(Cardnet = carte). Aucun gate ne les comparait à l'artefact : AJOUTER un évènement
|
||
(facturer un devis), en RETIRER un, RENOMMER un `base_field` ou CHANGER
|
||
`forma_pago_defaut` (Cardnet abandonné → code 2 Transfert) ferait dériver la prose
|
||
en silence — l'agent ERPNext Backend câblerait le mauvais évènement / la mauvaise
|
||
FormaPago (le risque même que la cross-cohérence veut prévenir), dérive qu'aucune
|
||
suite `tests/` (qui teste les FONCTIONS `ncf`/résolution, pas la prose) n'attrape.
|
||
On RECOMPUTE chaque ensemble/valeur depuis l'artefact (zéro duplication du contrat
|
||
du générateur · #6) et on exige que la prose l'encode exactement (absent ET en trop
|
||
via set-diff · accents/casse normalisés). Cohérences croisées en bonus (mordent un
|
||
plan **internement incohérent**) : `forma_pago_defaut.code` ∈ catalogue
|
||
`formas_pago` · libellé prose ⊆ libellé catalogue du code · ensembles non vides.
|
||
7 morsures vérifiées : prose retire `contrat` des émetteurs · prose renomme
|
||
`montant_contrat` · prose FormaPago `3→2` (drift artefact) · artefact
|
||
`forma_pago_defaut.code 3→2` (prose périmée) · artefact ajoute l'évènement `devis`
|
||
(prose sans `devis` — états ET base_field mordus) · FormaPago prose supprimée
|
||
(INTROUVABLE) · artefact `code→99` hors catalogue (incohérence interne) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur l'**identité du RÔLE émetteur** e-CF (**3e surface** du MÊME
|
||
README `fiscal/ecf_dgii`) — le seul rôle habilité à ÉMETTRE un e-CF. Le bloc e-NCF
|
||
gate le format, le bloc cross-cohérence gate états/champs/FormaPago ; mais aucun ne
|
||
comparait l'IDENTITÉ de ce rôle à ses deux sources. Le bullet `role_id` (README:51-54)
|
||
le NOMME par trois attributs data-derived : son `role_id` « `compta-fiscaliste-ecf` »
|
||
= le rôle que TOUS les `emission_events` d'`out/ecf_plan.json` (byte-gaté) référencent,
|
||
son `erpnext_role_name` « _OTO Compta Fiscaliste eCF_ », et son `portail` « `compta` »
|
||
résolu du contrat `rbac_50_roles.json` (byte-gaté par les 3 MANIFEST). RÉAFFECTER
|
||
l'émission à un autre rôle (dans le plan), le RENOMMER, ou DÉPLACER
|
||
`compta-fiscaliste-ecf` du portail `compta` vers un autre (dans le contrat : l'e-CF
|
||
serait émis HORS Compta — la séparation même que la cross-cohérence pose) ferait
|
||
dériver la prose en silence, dérive qu'aucune suite `tests/` (qui teste les FONCTIONS
|
||
`ncf`/résolution, pas la prose) n'attrape. On RECOMPUTE `role_id`/`erpnext_role_name`
|
||
de l'artefact et `portail`/nom Frappe du contrat (zéro duplication) et on exige la
|
||
prose EXACTE. Cohérences croisées en bonus (mordent un plan/contrat **internement
|
||
incohérent**) : tous les `emission_events` pointent UN SEUL rôle (fonction · non vide) ·
|
||
ce `role_id` existe UNE fois au contrat · le nom résolu dans l'artefact == le contrat
|
||
(zéro-dup) · `portail == compta`. 6 morsures vérifiées : prose renomme le `role_id` ·
|
||
prose renomme le nom Frappe · prose déplace le portail (`compta→ventes`) · artefact
|
||
réaffecte les deux `emission_events` à `ventes-directeur` (5 mords en cascade) ·
|
||
contrat déplace le rôle vers `ventes` (portail HORS Compta + prose périmée) · artefact
|
||
diverge du contrat sur le nom (zéro-dup cassé) ; un claim absent échoue AUSSI ;
|
||
restauré = vert.
|
||
|
||
Même classe encore sur le **jeu de DEVISES `moneda`** (**4e surface** du MÊME
|
||
README `fiscal/ecf_dgii`) — **ancré sur CLAUDE.md #10**. `moneda.options = ["USD",
|
||
"DOP"]` — les devises que `TipoMoneda` peut porter dans un e-CF — vit dans le spec ET,
|
||
byte-gaté, dans `out/ecf_plan.json` ; c'est une valeur CANONIQUE de #10 (« **USD +
|
||
DOP** devises ») dont le README se réclame « #10 » (README:37/57/77). Or **byte-gater
|
||
une constante prouve la REPRODUCTIBILITÉ, PAS l'ANCRAGE** : `check_artifacts` prouve
|
||
`ecf_plan==build` depuis le spec, jamais que `spec[moneda]==#10`. Les blocs fiscaux
|
||
amont gatent le FormaPago (Cardnet · #10) et les états/champs émetteurs, AVEUGLES au
|
||
jeu de devises ; le bloc « Fiches #10 » ancre la prose des fiches ERPNext/CRM, AVEUGLE
|
||
au module fiscal ; et le `source` du bloc `moneda` est legit. `null` (il source le
|
||
**taux TipoCambio**, à confirmer côté VPS · #8), donc l'ancre des OPTIONS vit dans un
|
||
champ DISTINCT `options_source`. RENOMMER/ÉTENDRE les devises dans CLAUDE.md #10
|
||
(`USD + DOP` → `USD + EUR`) ET dans le spec de façon cohérente reste byte-VERT tout en
|
||
faisant émettre à l'e-CF un `TipoMoneda` d'une devise qui CONTREDIT le mandat — « vert
|
||
trompeur » de la classe de la marque SEO §Entités, la persona Chat OTOIA ou les tokens
|
||
branding #4, qu'aucune suite `tests/` (FONCTIONS de génération, jamais l'ancre à
|
||
CLAUDE.md) n'attrape. On RÉUTILISE `exp["devises"]` déjà recomputé depuis #10 (zéro
|
||
duplication) et on exige : (a) `spec[moneda].options` == devises de #10 en ORDRE exact
|
||
(mord ajout/retrait/réordonnancement) ; (b) l'artefact byte-gaté idem ; (c) le spec
|
||
DÉCLARE l'ancrage — `options_source` cite `CLAUDE.md #10` + chaque devise ; (d) la
|
||
ligne README qui se réclame de #10 cite le jeu EXACT + toute mention `moneda`/`devise`
|
||
d'une paire de devises == le jeu (aucune périmée). 7 morsures vérifiées : CLAUDE.md #10
|
||
`USD+DOP→USD+EUR` (l'ancre est vive → mord spec+artefact+déclaration+README en cascade) ·
|
||
spec `options USD/DOP→USD/EUR` régénéré (le vrai *silent green*) · artefact seul
|
||
`USD→EUR` (spec+README périmés) · reorder spec `[DOP,USD]` (ordre exact) · `options_source`
|
||
sans « CLAUDE.md #10 » (ancre déclarée manquante) · README ligne #10 `USD/DOP→USD/EUR`
|
||
(prose périmée) · README table `moneda USD/DOP→USD/EUR` (mention périmée) ; un claim
|
||
absent échoue AUSSI ; restauré = vert.
|
||
|
||
Même classe enfin sur la **cross-cohérence du barème de commissions** (livrable
|
||
`crm/commissions`) — le CŒUR du module. La section « Cross-cohérence » de son README
|
||
énumère **à la main** deux ensembles data-derived + une propriété : les états
|
||
**brouillon exclus** « `lead/visite/devis/abandonné` » (README:47), les **champs de
|
||
base** « `montant_reservation`, `montant_contrat` » (README:50) et l'invariant « on
|
||
ne commissionne QUE des états **soumis** (`doc_status = 1`) » (README:46). Le bloc
|
||
racine ne gate que le **compte de tests** commissions (« 25 », via l'agrégat « Total
|
||
CRM ») ; ces ensembles n'étaient recomputés par AUCUN gate. Or ils sont
|
||
**data-derived** : les brouillons = `{update_value | doc_status == 0}` de
|
||
`crm/workflow_vente/out/workflow.json`, les champs de base = `{base_field}` de
|
||
`crm/commissions/out/commission_plan.json` (les deux byte-gatés par
|
||
`check_artifacts`). PROMOUVOIR un état brouillon en soumis, AJOUTER/RENOMMER un champ
|
||
de base ou ÉCHANGER un état ferait dériver la prose en silence pendant que l'artefact
|
||
dit autre chose — dérive qu'aucune suite `tests/` (qui teste des FONCTIONS
|
||
finance/résolution, pas la prose) n'attrape. On RECOMPUTE chaque ensemble depuis SON
|
||
artefact (zéro duplication du contrat du générateur · #6) et on exige que la prose
|
||
l'énumère **exactement** (absent ET en trop mordus via set-diff · accents/casse
|
||
normalisés). Cohérence croisée en bonus (mord un plan **internement incohérent** avec
|
||
le workflow) : tout `update_value` commissionné ∈ états soumis ET ∉ brouillons —
|
||
l'**invariant 3** du module, non vide. 5 morsures vérifiées : échange brouillon
|
||
(`abandonné→perdu`) · sous-ensemble des champs de base (retrait `montant_contrat`) ·
|
||
énumération des brouillons supprimée (INTROUVABLE) · plan pointant un brouillon
|
||
(`reservation→lead`, invariant 3 cassé) · promotion de l'état `abandonne` en soumis
|
||
dans l'artefact (draft set change, « en trop » dans la prose) ; restauré = vert.
|
||
|
||
Autre surface du MÊME livrable `crm/commissions`, la plus grave possible : sa
|
||
**promesse ANTI-INVENTION (#6)** — la RAISON D'ÊTRE du module. Aucun taux de
|
||
commission n'étant documenté dans CLAUDE.md, le README §« Anti-invention » promet
|
||
(README:22-23) que « le barème livré porte `taux_pct: null` + `source: null` +
|
||
`a_confirmer: true` pour **chaque** évènement », et l'invariant CLI #7 REFUSE tout
|
||
`taux_pct` fourni **sans `source`**. La cross-cohérence ci-dessus ne gate QUE
|
||
brouillons/champs de base ; RIEN ne confrontait cette PROMESSE à l'artefact. Or
|
||
c'est la dérive la plus dommageable du module : qu'un `taux_pct` DEVIENNE non-null
|
||
(taux INVENTÉ, ou valeur remplie SANS `source`) pendant que le README affirme
|
||
encore « tout `null` » → le calcul `base × taux` émettrait une commission fondée
|
||
sur un chiffre inventé (l'invention EXACTE que le module promet d'écarter). On
|
||
RECOMPUTE l'état null/confirmé depuis `commission_plan.json` (byte-gaté) et on
|
||
exige que la promesse du README le REFLÈTE **dans les deux sens** (si la Direction
|
||
confirme un jour un taux, la promesse « tout null » DOIT être mise à jour, sinon
|
||
ROUGE — le gate suit la vie du module sans faux négatif). Cohérences croisées :
|
||
invariant #7 permanent (aucun `taux_pct` sans `source`) · `a_confirmer` ⟺
|
||
`taux_pct is null` · `MANIFEST.counts` recoupe le plan (`taux_a_confirmer` == nb à
|
||
confirmer · `evenements` == |plan|). 5 morsures vérifiées : `taux_pct` rempli SANS
|
||
source (invariant #7 · invention) · `taux_pct` rempli AVEC source (a_confirmer
|
||
resté « à confirmer » + promesse README périmée) · `a_confirmer=false` sur taux
|
||
null · `MANIFEST.counts.taux_a_confirmer` périmé (5→4) · promesse anti-invention
|
||
retirée du README (INTROUVABLE) ; restauré = vert.
|
||
|
||
Troisième surface du MÊME livrable `crm/commissions`, le « **à quel rôle** » de la
|
||
question fondatrice : la §Cross-cohérence du README promet que chaque `role_id`
|
||
commissionné « doit être résolu depuis `rbac_50_roles.json` … ET appartenir au
|
||
portail `ventes` » (invariant CLI #5) avec un `erpnext_role_name` « cohérent avec
|
||
RBAC » (invariant #6 · le « nom Frappe résolu » du README:36). C'est une
|
||
**séparation des pouvoirs** : on ne commissionne QUE des rôles du portail ventes.
|
||
Les deux blocs commissions ci-dessus gatent les états/champs/taux, mais RIEN ne
|
||
confrontait cette appartenance-portail ni la résolution du nom à l'artefact. On
|
||
RECOMPUTE depuis `commission_plan.json` + `rbac_50_roles.json` + `MANIFEST.json`
|
||
(byte-gatés · zéro dup du contrat du générateur) : (a) chaque `role_id` payé ∈
|
||
contrat (jamais fantôme) · (b) `erpnext_role_name` du plan == contrat (nom Frappe
|
||
non figé en dur) · (c) portail des rôles payés = **singleton** ∈ `portails_business`
|
||
== le portail `ventes` nommé au README (payer un rôle compta/construction romprait
|
||
le singleton ; renommer le portail au contrat désaccorderait le littéral README) ·
|
||
(d) `MANIFEST.roles_rbac_utilises` == l'ensemble distinct `(role_id, nom)` réellement
|
||
payé, `counts.roles` recoupé. 6 morsures vérifiées : `role_id` fantôme absent du
|
||
contrat · `erpnext_role_name` dérivé dans le plan · rôle **compta** commissionné
|
||
(pouvoirs mélangés) · littéral portail README `ventes→sales` · `roles_rbac_utilises`
|
||
amputé · renommage du rôle DANS le contrat (cascade nom + MANIFEST) ; restauré = vert.
|
||
|
||
Quatrième surface du MÊME livrable `crm/commissions`, le « **sur quelle devise** » du
|
||
calcul : la §Cross-cohérence promet (README:54) que « `devise_field` = le champ
|
||
`devise` (`Select` **USD/DOP** · #10) du DocType ». C'est l'invariant CLI #8 : le plan
|
||
porte la clé racine `devise_field == "devise"`, le DocType Dossier Vente
|
||
(`doctype_spec.json`) porte bien un champ `devise` de fieldtype `Select`, et ses
|
||
`options` == les DEUX devises canoniques de **CLAUDE.md #10** (« **USD + DOP**
|
||
devises »). Les trois blocs commissions ci-dessus gatent états/champs/rôles/taux, mais
|
||
RIEN ne touchait ce couple champ↔devises — surface distincte du bloc « Fiches #10 »
|
||
(prose ERPNext/CRM) et du bloc e-CF `TipoMoneda` (options du spec fiscal). Dérive #6 :
|
||
RENOMMER le champ (`devise`→`moneda`), changer son fieldtype (`Select`→`Data` :
|
||
énumération PERDUE, montant en monnaie libre) ou altérer ses `options` (`USD/DOP`→
|
||
`USD/EUR` : devise HORS #10) ferait dériver la prose en silence pendant que
|
||
l'artefact/DocType dit autre chose — dérive qu'aucune suite `tests/` (FONCTIONS
|
||
finance/résolution, jamais la prose vs artefact) n'attrape. On RECOMPUTE depuis
|
||
`commission_plan.json` + `dossier_vente/doctype_spec.json` (byte-gatés) + CLAUDE.md
|
||
#10 : (0) ANCRAGE — les 2 devises depuis le segment gras « USD + DOP » de #10 · (a)
|
||
CROSS-ARTEFACT — `plan.devise_field == "devise"` == fieldname du champ DocType,
|
||
fieldtype `Select`, `options` (ORDRE inclus) == devises #10 · (b) PROSE — README:54
|
||
nomme le champ `devise`, le dit `Select`, cite `USD/DOP` == #10 et tague `#10`. 7
|
||
morsures vérifiées : README champ `devise→moneda` · README `Select→Data` · README
|
||
`USD/DOP→USD/EUR` · README tag `#10` retiré (INTROUVABLE) · **DocType** `options
|
||
USD→EUR` (hors #10) · **CLAUDE.md #10** `USD+DOP→USD+EUR` (ancrage vivant · cascade) ·
|
||
**plan** `devise_field devise→moneda` (invariant #8) ; restauré = vert.
|
||
|
||
Même classe enfin sur le **catalogue RBAC** (livrable `rbac/fixtures_gen`) — deux
|
||
faits data-derived transcrits **à la main** dans le README. (1) La liste des DocTypes
|
||
`custom` à créer avant import « (`Faisabilité`, `Publiciste Log`, `CONFOTUR
|
||
Application`, `API Access`) » (README §« Application sur VPS ») == l'ensemble
|
||
`custom_doctypes_a_confirmer` d'`rbac/fixtures_gen/out/MANIFEST.json` (les DocTypes
|
||
`custom: true` du contrat RBAC). (2) La **séparation des pouvoirs** — « le flag
|
||
`set_user_permissions` n'est émis que pour le rôle **RBAC Admin** » (README:48) ==
|
||
l'ensemble `{role | set_user_permissions == 1}` d'`out/custom_docperm.json`. Le bloc
|
||
racine ne gate que le **compte** (« 50 rôles / 116 DocPerm » via l'agrégat RBAC de
|
||
l'apply_plan) — jamais l'**identité** de ces deux ensembles. AJOUTER un DocType
|
||
`custom` au contrat, RENOMMER/ÉCHANGER l'un des 4 ou PROMOUVOIR un 2ᵉ rôle porteur du
|
||
flag (élévation de privilège) ferait dériver la prose en silence pendant que
|
||
l'artefact byte-gaté dit autre chose — dérive qu'aucune suite `tests/` (qui teste des
|
||
FONCTIONS de mapping, pas la prose du README) n'attrape. On RECOMPUTE chaque ensemble
|
||
depuis SON artefact (zéro duplication du contrat du générateur · #6) et on exige que
|
||
la prose l'énumère/le nomme **exactement** (absent ET en trop mordus via set-diff ·
|
||
accents/casse normalisés). Cohérences croisées en bonus : le catalogue est **non
|
||
vide, sans doublon et trié** (byte-déterminisme du générateur) et la séparation des
|
||
pouvoirs est un **singleton** (ni vide — garde vacante — ni multiple — élévation de
|
||
privilège). 6 morsures vérifiées : échange d'un nom de DocType (`Faisabilité →
|
||
Faisabilite2`) · sous-ensemble (retrait `API Access`) · rôle nommé faux (`RBAC Admin
|
||
→ Ventes Conseiller`) · énumération supprimée (INTROUVABLE) · 2ᵉ rôle promu au flag
|
||
dans l'artefact (singleton cassé) · catalogue MANIFEST non trié (byte-déterminisme) ;
|
||
restauré = vert.
|
||
|
||
Même classe enfin sur une **2ᵉ surface du README des portails** (`frontend/portails`) —
|
||
le §« Hand-off → agent ERPNext Backend » énumère **à la main** les DocTypes `custom` à
|
||
créer avant import « (`CONFOTUR Application`, `Faisabilité`, `Publiciste Log`) », un
|
||
catalogue DATA-DERIVED == `doctypes_custom_a_creer` d'`out/MANIFEST.json` (l'UNION des
|
||
`doctypes_custom` par workspace, eux-mêmes lus du flag `custom` du contrat RBAC pour les
|
||
seuls DocTypes **câblés** dans un portail — c'est donc un ensemble PROPRE au module,
|
||
sous-ensemble de celui de `rbac/fixtures_gen` : `API Access`, custom mais non câblé, en
|
||
est ABSENT). Le bloc portails ci-dessus ne gate que les **comptes** par workspace
|
||
(cartes/liens/rôles) — jamais ce catalogue de hand-off. AJOUTER une carte visant un
|
||
nouveau DocType custom au contrat, en RENOMMER un ou en RETIRER un du câblage laisse la
|
||
consigne périmée (l'agent Backend créerait le mauvais jeu de DocTypes → liens pointant
|
||
dans le vide, le risque même que la consigne prévient) — dérive qu'aucune suite `tests/`
|
||
n'attrape. On RECOMPUTE le catalogue depuis `MANIFEST` et on exige que la prose l'énumère
|
||
**exactement** (set-diff · accents/casse normalisés). Cohérences croisées en bonus :
|
||
catalogue **non vide, sans doublon et trié** (byte-déterminisme) · **égal à l'UNION** des
|
||
`doctypes_custom` par workspace (pas de DocType custom câblé mais oublié du hand-off, ni
|
||
l'inverse). 5 morsures vérifiées : prose renomme `Faisabilité → Faisabilite2` · prose
|
||
retire `Publiciste Log` (sous-ensemble) · énumération supprimée (INTROUVABLE) · MANIFEST
|
||
retire du catalogue un DocType encore câblé (incohérence interne : câblé hors catalogue) ·
|
||
catalogue MANIFEST non trié (byte-déterminisme) ; restauré = vert.
|
||
|
||
Enfin, mêmes racines pour les **paramètres canoniques du générateur de faisabilité**
|
||
(`faisabilite/generator`) : les 6 valeurs imposées à CHAQUE faisabilité — frais d'édition
|
||
`3 %`, marketing `8.5 %`, point d'équilibre `52 %` (CLAUDE.md #9) · devises `USD+DOP`,
|
||
format `Letter US`, paiement `Cardnet` (CLAUDE.md #10) — sont la garantie anti-invention
|
||
(#6) du module. Elles vivent, RECOPIÉES, dans `genlib/model.py::CANONICAL` (le dict émis
|
||
dans chaque rendu — **hors `out/` donc invisible à check_artifacts**) et dans la prose
|
||
README:28, **sans aucun ancrage**. Le seul contrôle existant
|
||
(`test_generator.py::test_canonical_params…`) assert que le RENDU contient « `3 %` »,
|
||
« `52 %` »… mais ces marqueurs sont **hardcodés dans le test** — une copie de plus, jamais
|
||
comparée à CLAUDE.md ; si la contrainte #9 change (`52 %→55 %`), model.py, le test ET le
|
||
README restent `52 %` : tous cohérents entre eux, TOUS contredisant CLAUDE.md → le
|
||
générateur émettrait `52 %` dans chaque faisabilité alors que le mandat dit `55 %`. Comme
|
||
le catalogue projets `dossier_vente` et le roadmap-anchor (INV11), on **ancre** les
|
||
consommateurs à leur source : on RECOMPUTE les 6 valeurs des lignes #9/#10 de `CLAUDE.md`
|
||
(source unique) et on exige qu'elles égalent EXACTEMENT (1) `model.py::CANONICAL` (% par
|
||
valeur numérique · textes accent/espace-insensibles) et (2) l'énumération de la prose
|
||
(set-diff : absent ET en trop). 7 morsures vérifiées : CLAUDE.md #9 `52 %→55 %` (model +
|
||
README périmés) · model.py marketing `8.5 %→9 %` · CLAUDE.md #10 `Cardnet→Azul` (model
|
||
périmé) · README retire `Letter US` (absent) · README ajoute un token `IBAN` (en trop) ·
|
||
énumération README supprimée (INTROUVABLE) · model.py `USD + DOP → USD` ; restauré = vert.
|
||
|
||
**2ᵉ surface du MÊME module** : trois copies DE PLUS des 6 marqueurs, longtemps
|
||
ungated (dont le commentaire ci-dessus NOMMAIT le défaut sans le fermer). (c)
|
||
`genlib/renderer.py::_CANONICAL_MARKERS` — la LISTE que `check_canonical()` parcourt
|
||
pour PROUVER que le rendu du volet ingénierie porte les 6 marqueurs : l'oracle de
|
||
l'enforcement lui-même. Si elle dérive de `CANONICAL` (donc de #9/#10) le contrôle
|
||
devient FAUX (exige un marqueur périmé ⇒ faux rouge ; ou on en retire un ⇒ vert
|
||
vacux). (d) les DEUX docstrings de `genlib/scorer.py` (barème axe 4 + fonction
|
||
`_score_parametres_canoniques`) qui énumèrent « 3 % / 8.5 % / 52 % · USD+DOP · Letter
|
||
US · Cardnet ». (e) l'oracle du test `tests/test_generator.py` (`for marker in [...]`).
|
||
On RÉUTILISE `exp` (zéro duplication) : les LISTES (c)/(e) sont parsées et comparées
|
||
à l'ensemble EXACT (absent ET en trop) ; les DOCSTRINGS (d) doivent porter CHAQUE
|
||
valeur normalisée (espaces/accents-insensibles ⇒ `USD+DOP`==`USD + DOP`). Morsures
|
||
vérifiées : renderer `Cardnet→Azul` (absent/en-trop) · test oracle retire `8.5 %`
|
||
(absent) · scorer docstring `52 %→55 %` (drift) · **CLAUDE.md #10 `Cardnet→Azul`
|
||
mord SIMULTANÉMENT model.py + renderer + test + les DEUX docstrings** (la valeur
|
||
même de l'ancrage) ; restauré = vert.
|
||
|
||
Même classe enfin sur QUATRE copies **hors du module generator** — la prose #9 de
|
||
DEUX modules distincts qui se réclame explicitement « (#9) » / « CLAUDE.md #9/#10 »
|
||
mais qu'aucun gate ne liait à la contrainte (corriger le generator ne les touche pas) :
|
||
(a) `faisabilite/bancable/README.md` et (b) sa docstring `banclib/finance.py`
|
||
(« 3 % édition » et « 8.5 % marketing » (#9)), (c) `banclib/deps.py`
|
||
(« CLAUDE.md #9/#10 (3 %/8.5 %/52 % · USD+DOP · Cardnet · Letter US) », 6 marqueurs),
|
||
(d) `crm/commissions/README.md` (« pourcentages canoniques (3 % édition · 8.5 %
|
||
marketing · 52 % point d'équilibre) »). On RÉUTILISE `exp` (zéro re-parse). Morsures
|
||
vérifiées : `#9 8.5 %→9 %` mord les 4 · `#9 52 %→55 %` mord (c)+(d) SEULS ((a)/(b) ne
|
||
citent qu'édition+marketing ⇒ correctement épargnées, ciblage précis) · `#10
|
||
Cardnet→Azul` mord l'énumération (c). Un claim absent échoue AUSSI (traçabilité #6).
|
||
|
||
Même classe enfin sur la **fiche d'identité du rôle RBAC mobile** — la fiche agent
|
||
`03_agents/mobile/AGENT.md`. Les builds/soumissions stores étant **hors-repo** (#8),
|
||
l'agent mobile le reconnaît et déclare que son **seul ancrage in-repo vérifiable** est
|
||
le rôle `plateforme-mobile` du contrat `rbac_50_roles.json` ; sa ligne « Points de
|
||
contact réellement commités » en recopie donc **à la main** TOUS les attributs
|
||
data-derived : `erpnext_role_name` (« OTO Plateforme Mobile »), `nom_fr`
|
||
(« Développeur Mobile »), `famille`/`portail` (`plateforme`), `entite_principale`
|
||
(`9060 QC`), `niveau` (`2`), `scope_donnees` (`groupe`), `modules` (`Core`+`Website`),
|
||
la **permission** `API Access` custom R/W et la `description`. C'est la carte que
|
||
l'agent ERPNext Backend lit pour **seeder le rôle** (`seed_mobile_rbac.py`). Aucun gate
|
||
ne comparait ces valeurs au contrat : le README `mobile/app_config` n'est gaté que sur
|
||
des **comptes agrégés** (« 5 onglets · 44 rôles »), aveugles à l'identité de CE rôle.
|
||
Élever le `scope_donnees` (`groupe→entite` : sur-portée inter-entités), changer le
|
||
`niveau`/l'`entite_principale`, renommer le rôle, ajouter/retirer un module ou élargir
|
||
la permission (`R/W→R/W/D`) laisserait la fiche périmée en silence pendant que le
|
||
contrat dit autre chose → l'agent câblerait le mauvais rôle (l'invention même que #6
|
||
interdit), dérive qu'aucune suite `tests/` (qui teste des FONCTIONS de résolution RBAC,
|
||
pas la prose d'une fiche) n'attrape. On RECOMPUTE chaque attribut depuis le contrat
|
||
(zéro duplication · #6) et on exige la prose EXACTE par attribut ; le sigle de
|
||
permission est **canonique et ordonné** (C/R/W/D/S/X/A) de sorte qu'élargir OU rétrécir
|
||
les verbes change le sigle attendu (un préfixe `R/W` ne satisfait plus `R/W/D`). La
|
||
`description` est recoupée **verbatim**, emphase markdown et point final neutralisés.
|
||
Cohérences croisées : `famille == portail` (bijection portail du contrat), la
|
||
permission est bien `custom`, et le `role_id` figure **une seule fois** dans le contrat.
|
||
12 morsures vérifiées (6 côté contrat, 6 côté fiche) : contrat `scope groupe→entite` ·
|
||
contrat `niveau 2→3` · contrat `+delete` (sigle `R/W→R/W/D`) · contrat description
|
||
réécrite · contrat rôle renommé · fiche `modules Website→Selling` · fiche
|
||
`entité 9060 QC→WAF` · fiche perm `R/W→R/W/D` (sur-revendication) et `R/W→R`
|
||
(sous-revendication) · fiche modules over/under-claim · ligne de rôle supprimée
|
||
(INTROUVABLE) ; restauré = vert.
|
||
|
||
Même classe encore sur les **deux fiches d'identité des rôles RBAC faisabilité** —
|
||
`03_agents/rendu/AGENT.md` (rôle `faisabilite-rendu-3d`) et
|
||
`03_agents/ifc_speckle/AGENT.md` (rôle `faisabilite-ifc-speckle`). Chaque ligne
|
||
« Contact in-repo (vérifié) » recopie **à la main**, du contrat `rbac_50_roles.json`,
|
||
les attributs data-derived RESTITUÉS par la fiche : `erpnext_role_name`, `portail`
|
||
(`construction`), `entite_principale` (`WA SRL`), `niveau` (`2`), plus `nom_fr` &
|
||
`modules` (rendu) et surtout les **deux `permissions_cibles`** — `File` R/W/create +
|
||
`Faisabilité` R (rendu, lecture seule) ou R/W (ifc). C'est la carte que l'agent ERPNext
|
||
Backend lit pour seeder le rôle porteur des rendus / de l'export IFC→GLB. Aucun gate ne
|
||
comparait ces valeurs au contrat : le README `frontend/portails` n'est gaté que sur des
|
||
**comptes de rôles par Workspace**, aveugles à l'identité de CE rôle. Élargir
|
||
`Faisabilité` R→R/W (le rôle Rendu 3D gagnerait l'écriture sur un DocType qu'il ne doit
|
||
que LIRE), changer le `niveau`/l'`entite_principale`, renommer le rôle ou permuter un
|
||
module laisserait la fiche périmée en silence. On RECOMPUTE chaque attribut du contrat
|
||
(zéro duplication · #6) ; les verbes de permission sont rendus en **ordre canonique**
|
||
(read→R · write→W · create/delete/… en toutes lettres) de sorte qu'élargir OU rétrécir
|
||
change la chaîne attendue (`R/W/create` ne satisfait plus `R/W/create/delete`).
|
||
**Différence notable avec la fiche mobile** : la `description` de ces deux fiches est
|
||
une **paraphrase éditoriale** (le contrat porte « (… — CLAUDE.md) » / « les modèles »
|
||
que la fiche condense) — elle n'est donc **pas** gatée verbatim (ne rien réécrire de
|
||
correct pour satisfaire un gate · #6) ; seuls les attributs **structurés**, transcrits à
|
||
l'exact, sont contraints. 11 morsures vérifiées (5 côté contrat, 6 côté fiche) : contrat
|
||
rendu `Faisabilité +write` (sigle `R→R/W`) · contrat rendu `niveau 2→3` · contrat rendu
|
||
`WA SRL→WAF` · contrat rendu rôle renommé · contrat ifc `Faisabilité R/W→R` · fiche
|
||
rendu perm `File R/W/create→…/delete` · fiche ifc perm `R/W→R/W/create` · fiche rendu
|
||
module `Faisabilité→Selling` · fiche ifc `WA SRL→WAF` · fiche rendu `nom_fr` retiré
|
||
(claim absent) · ligne de contact ifc supprimée (INTROUVABLE) ; restauré = vert.
|
||
|
||
Enfin, sur une surface data-derived **DISTINCTE du contrat** que les trois fiches
|
||
précédentes touchent aussi : le **rattachement Workspace** (`Has Role`) de leur rôle —
|
||
QUELLE CONSOLE (Workspace ERPNext natif v15) le rôle peut atteindre. Cette membership ne
|
||
vit PAS dans `rbac_50_roles.json` mais dans `frontend/portails/out/workspace.json`
|
||
(byte-gaté par `check_artifacts`), et chaque fiche l'affirme EN PROSE :
|
||
`03_agents/rendu/AGENT.md:35` — rôle « OTO Faisabilité Rendu 3D » **rattaché au
|
||
Workspace `OTO Construction`** ; `03_agents/ifc_speckle/AGENT.md:35` — rôle « OTO
|
||
Faisabilité IFC Speckle » **rattaché à un Workspace** (générique) ; et l'assertion
|
||
**NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle « OTO Plateforme
|
||
Mobile » **volontairement PAS rattaché au Workspace `OTO Ventes`**. Aucun gate ne les
|
||
couvrait : le bloc « triplets par workspace » gate le **compte** de rôles par Workspace
|
||
(`nb_roles`), aveugle à QUEL rôle ; les blocs Fiches Faisabilité / Fiche Mobile gatent le
|
||
**contrat**, pas la membership `Has Role`. Rattacher le rôle mobile à `OTO Ventes`
|
||
(**sur-exposition console** : le dev mobile gagnerait l'accès au portail Ventes — la
|
||
restriction même que la note #6 pose), retirer le rôle rendu de `OTO Construction`
|
||
(perte d'accès console du spécialiste) ou le **déplacer** vers un autre Workspace
|
||
laisserait la fiche périmée en silence. On RECOMPUTE, depuis `workspace.json`, l'ensemble
|
||
des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat
|
||
par `id`, zéro duplication · #6), et on confronte l'assertion : **in_named** (rendu) —
|
||
membership == {le Workspace nommé} ET la prose le cite ; **in_any** (ifc) — membership
|
||
≥ 1 Workspace ; **not_in_named** (mobile) — rôle ABSENT du `Has Role` du Workspace nommé.
|
||
Un Workspace nommé mais **absent** de l'artefact (typo) échoue avant tout (anti-cible
|
||
fantôme). 9 morsures vérifiées (4 côté artefact, 5 côté fiche/bord) : artefact ajoute le
|
||
rôle mobile à `OTO Ventes` (sur-exposition #6 VIOLÉE) · artefact retire le rôle rendu de
|
||
`OTO Construction` · artefact déplace rendu vers `OTO Achat` · artefact retire ifc de
|
||
tous les Workspaces · fiche rendu renomme le Workspace (`OTO Construction→OTO Ventes`,
|
||
INTROUVABLE) · fiche rendu supprime l'assertion · fiche mobile nomme un Workspace
|
||
fantôme (`OTO Marketing`) · fiche ifc supprime l'assertion générique · fiche mobile passe
|
||
le négatif au positif (retire « volontairement pas ») ; restauré = vert.
|
||
|
||
Enfin, sur une **3e surface d'appartenance** encore DISTINCTE des deux précédentes :
|
||
l'appartenance à `roles_allowed` du **chat OTOIA** — QUI peut **invoquer** l'assistant
|
||
embarqué dans un portail. Ce contrôle d'accès ne vit ni dans `rbac_50_roles.json`
|
||
(attributs) ni dans `workspace.json` (`Has Role`, accès console) mais dans
|
||
`frontend/chat_otoia/out/chat_mount.json` (une config runtime par portail, byte-gaté
|
||
par `check_artifacts`), et deux fiches l'affirment EN PROSE : `03_agents/rendu/AGENT.md:36`
|
||
— rôle « OTO Faisabilité Rendu 3D » **fait partie des `roles_allowed` du portail
|
||
`construction`** (droit d'invoquer le chat pour déclencher les rendus assistés) ; et
|
||
l'assertion **NÉGATIVE d'honnêteté** (#6) de `03_agents/mobile/AGENT.md:41` — rôle
|
||
« OTO Plateforme Mobile » (famille `plateforme`) **volontairement dans AUCUNE
|
||
`roles_allowed`** (le chat ne couvre que les portails métier). Aucun gate ne les
|
||
couvrait : le bloc Chat OTOIA amont gate l'**identité** de la persona/capabilities
|
||
(ancrée CLAUDE.md) et la **composition** (5 mounts ⇔ 5 portails), aveugle à QUEL rôle
|
||
figure dans quelle `roles_allowed` ; le bloc Fiches Workspace gate un artefact + une
|
||
sémantique DIFFÉRENTS (accès console ≠ droit d'invocation du chat). On RECOMPUTE, depuis
|
||
`chat_mount.json`, l'ensemble des portails dont la `roles_allowed` porte
|
||
l'`erpnext_role_name` du rôle (résolu du contrat par `id`, zéro duplication · #6), et on
|
||
confronte l'assertion : **in_named_portail** (rendu) — le portail nommé est data-derived
|
||
(== le `portail` du rôle au contrat, **triple ancrage** : portail nommé == `portail`
|
||
contrat == unique portail dont la `roles_allowed` le porte) et membership == {ce portail}
|
||
ET la prose le cite avec le droit « invoquer le chat OTOIA » ; **not_in_any** (mobile) —
|
||
rôle ABSENT de TOUTE `roles_allowed`. Un portail nommé mais **absent** des mounts (typo)
|
||
échoue avant tout (anti-cible fantôme). 6 morsures vérifiées (3 côté artefact, 3 côté
|
||
fiche/bord) : artefact retire le rôle rendu de la `roles_allowed` construction (perte du
|
||
déclenchement assisté) · artefact ajoute le rôle mobile à `ventes` (**SUR-EXPOSITION** du
|
||
chat #6 VIOLÉE) · artefact ajoute AUSSI rendu à `compta` (droit élargi, la fiche nomme UN
|
||
seul portail) · fiche rendu nomme le mauvais portail (`construction→compta`, INTROUVABLE) ·
|
||
fiche mobile retire le token `roles_allowed` de l'assertion négative (INTROUVABLE) ;
|
||
restauré = vert.
|
||
|
||
**Tokens design canoniques `publiciste/branding` ancrés sur CLAUDE.md #4** : la
|
||
contrainte NON-NÉGOCIABLE #4 fixe l'identité visuelle luxury — fond dark `#0a0a12`
|
||
+ accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond**. Ces 4
|
||
tokens sont RECOPIÉS à **quatre** endroits jamais gatés : (a) les CONSTANTES
|
||
`lib/branding.py` (`COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`) — la source
|
||
unique importée par le générateur ET substituée dans le gabarit (`{{COLOR_BG}}…`),
|
||
hors `out/` donc **invisible à `check_artifacts`** ; (b) le **docstring** du même
|
||
fichier ; (c) le README (ligne du gabarit) ; (d) le **tuple d'oracle HARDCODÉ** de
|
||
`tests/test_publiciste.py` (`("#0a0a12","#f0b429","Fraunces","Cormorant Garamond")`).
|
||
Le seul contrôle existant (`test_marque_luxury`) assert que le RENDU contient ces
|
||
marqueurs, mais ils sont une **copie de plus, jamais comparée à CLAUDE.md** : si #4
|
||
change (accent `#f0b429`→`#c9a227`) et qu'on aligne `branding.py`, l'oracle du test
|
||
reste périmé (rouge trompeur) ; si RIEN ne bouge, tout reste vert alors que le site
|
||
public émettrait la mauvaise couleur — invention silencieuse (#6) qu'aucune suite
|
||
`tests/` (qui teste des FONCTIONS, pas l'ancrage à CLAUDE.md) n'attrape. **Même
|
||
patron EXACT** que les paramètres canoniques `faisabilite/generator` (ancrage
|
||
`CANONICAL`↔CLAUDE.md #9/#10). On RECOMPUTE les 4 tokens depuis la ligne #4 de
|
||
CLAUDE.md (zéro duplication) et on exige : (a) `branding.py` CONSTANTES == #4 (hex
|
||
insensible à la casse, fontes à l'exact) ; (b) docstring cite les 4 EXACTS ; (c)
|
||
README cite les 2 fontes ; (d) l'oracle du test == #4 (set-diff). **6 morsures
|
||
vérifiées** : `branding.py` accent `#f0b429→#e0a420` · `branding.py` display
|
||
`Fraunces→Playfair Display` · **CLAUDE.md #4** accent `→#c9a227` (mord SIMULTANÉMENT
|
||
les 3 copies aval : constante + docstring + oracle du test — la valeur de l'ancrage)
|
||
· docstring retire l'accent de la prose · README échange une fonte
|
||
(`Cormorant Garamond→Georgia`) · oracle du test retire `Fraunces` ; restauré = vert.
|
||
|
||
**Colonne « Job CI » des fiches agents == section `jobs:` de ci.yml** : chaque
|
||
table de livrables de `03_agents/*/AGENT.md` porte une colonne **Job CI** qui NOMME,
|
||
entre backticks, le job Gitea Actions exécutant la suite du module
|
||
(`rbac-tests`, `fiscal-ecf-tests`, `frontend-portails-tests`…). Ce nom est **saisi
|
||
à la main** ; la source de vérité est la clé `jobs:` de `.gitea/workflows/ci.yml`.
|
||
Les gates existants ne couvraient PAS ce lien : `check_ci_integrity.sh` prouve que
|
||
chaque job est CÂBLÉ dans `gate.needs` (INV-A/B), et les blocs amont recomputent les
|
||
**comptes** de tests par suite — mais RIEN ne vérifiait que le nom écrit dans la
|
||
fiche DÉSIGNE un job réel. Renommer un job dans ci.yml (`rbac-tests`→`rbac-role-tests`)
|
||
ou mal recopier `demo-scenario-tests` en `demo-scenarios-tests` laisse la fiche
|
||
pointer un job **fantôme** : le CI reste vert (le vrai job tourne sous son nouveau
|
||
nom), la doc d'identité de l'agent ment en silence. On RECOMPUTE l'ensemble des jobs
|
||
depuis la section `jobs:` de ci.yml (la section `on:` — push/pull_request — est AVANT
|
||
et donc exclue ; jamais une liste à la main) et on exige que chaque token `*-tests`
|
||
cité dans une fiche y figure. Direction **fiche→ci.yml** (le consommateur) : ci.yml
|
||
peut définir des jobs non cités (légitime), mais une fiche ne peut pas citer un job
|
||
absent. **33 références vérifiées** ; **2 morsures** : token de fiche
|
||
`seo-tests→seo-suite-tests` (job fantôme) · job ci.yml `rbac-tests` renommé (mord
|
||
SIMULTANÉMENT les 4 fiches qui le citent — erpnext_backend/ifc_speckle/mobile/rendu) ;
|
||
la disparition totale de la colonne échoue AUSSI ; restauré = vert.
|
||
|
||
**Persona & capabilities du Chat OTOIA ancrées sur CLAUDE.md §Architecture cible.**
|
||
Le générateur `frontend/chat_otoia` monte l'assistant conversationnel OTOIA (persona
|
||
**Amélie QC**) dans **chaque portail rôle** ; `out/chat_mount.json` (5 configs runtime)
|
||
et `out/MANIFEST.json` portent la persona `{nom: "Amélie", voix: "multilingual_v2"}`
|
||
et les 4 capabilities `aec.py`+`knowledge.py`+`prompt_engine.py`+`chat.py`. La
|
||
byte-gate `check_artifacts` prouve seulement que ces artefacts **se reconstruisent**
|
||
depuis `chat_spec.json` — **pas** que cette persona/ces capabilities == ce que
|
||
**nomme** CLAUDE.md §Architecture cible (« Voix Amélie QC (multilingual_v2) » ·
|
||
« OTOIA capabilities : aec.py + knowledge.py + prompt_engine.py + chat.py »). Le bloc
|
||
chat_otoia amont ne gate que la **composition** (portails métier · 1 block⇔1 mount) ;
|
||
le **seul** contrôle d'identité vit dans `tests/` (`test_persona_amelie_sourcee` /
|
||
`test_capabilities_sourcees_sans_ajout`) mais son **oracle est hardcodé** (`"Amélie"`,
|
||
`["aec.py", …]`) — une **copie de plus, jamais comparée à CLAUDE.md**. Piège #6 :
|
||
renommer la persona (`Amélie`→`Sophie`), changer la voix, ou renommer/retirer une
|
||
capability dans `chat_spec.json` (ou dans CLAUDE.md §Architecture cible) laisse le chat
|
||
monté dans **chaque portail** annoncer une persona/voix/capabilities qui **contredit le
|
||
mandat** pendant que la byte-gate **et** les tests restent **verts** (l'artefact se
|
||
reconstruit fidèlement depuis un spec dérivé ; l'oracle du test est une copie) — même
|
||
« vert trompeur » que les tokens branding ancrés sur #4 et la marque SEO §Entités. On
|
||
**re-dérive** persona+capabilities de CLAUDE.md §Architecture cible (source unique) et
|
||
on exige : **(a)** `chat_spec.json` persona/capabilities == CLAUDE.md ; **(b)** le spec
|
||
**déclare** l'ancrage (`persona.source` + chaque `capability.source` citent CLAUDE.md) ;
|
||
**(c)** **chacun** des 5 mounts + le MANIFEST portent persona/capabilities == CLAUDE.md ;
|
||
**(d)** le README cite l'ancre §Architecture cible + nomme persona + chaque capability
|
||
(la notation compacte `aec/knowledge/prompt_engine/chat.py` matchée par radical) ;
|
||
**(e)** l'**oracle du test** == CLAUDE.md (set-diff : absent ET en trop). **8 morsures
|
||
vérifiées** : CLAUDE.md `Amélie→Sophie` (mord simultanément spec/mount/README/test —
|
||
l'ancre est vive) · CLAUDE.md retire `chat.py` · spec `aec.py→exfil.py` (capability
|
||
injectée) · spec `persona.source` sans CLAUDE.md · **un seul** mount à voix altérée
|
||
(byte-gate aveugle) · README retire un radical · oracle du test persona `Amélie→Bob` ·
|
||
oracle du test capabilities réordonné/tronqué ; restauré = vert.
|
||
|
||
**Pied de page « Voix Amélie QC (multilingual_v2) » des fiches agents ancré sur
|
||
CLAUDE.md §Architecture cible.** 11 des 13 fiches `03_agents/*/AGENT.md` (toutes sauf
|
||
`faisabilite`/`publiciste`, qui n'orchestrent pas OTOIA) se terminent par « - Voix
|
||
**Amélie QC** (**multilingual_v2**) pour toute interaction OTOIA », recopiant à la main
|
||
les **trois** tokens de CLAUDE.md §Architecture cible « Voix Amélie QC (multilingual_v2) » :
|
||
le prénom persona (`Amélie`), le qualificatif de marque (`QC`) et le modèle de voix
|
||
ElevenLabs (`multilingual_v2`). Le bloc persona ci-dessus re-dérive bien persona+voix de
|
||
CLAUDE.md **mais ne confronte que le module `chat_otoia`** (spec + `out/` + son propre
|
||
README + l'oracle du test) — **aveugle aux 11 pieds de page** ; le bloc « configs
|
||
runtime » note même que le « QC » est « gardé en littéral non gaté ici ». Piège #6 : le
|
||
jour où ElevenLabs déprécie `multilingual_v2` (→`multilingual_v3`) ou qu'on renomme la
|
||
persona/le qualificatif dans CLAUDE.md, ces 11 fiches annonceraient **en silence** une
|
||
voix/persona que le mandat ne prescrit plus, chacune affirmant orchestrer « toute
|
||
interaction OTOIA » avec le mauvais modèle — aucune suite `tests/` (fonctions du builder,
|
||
jamais la prose des fiches) ne l'attrape. On **re-dérive indépendamment** le triplet
|
||
(persona · qualif · voix) de CLAUDE.md §Architecture cible (regex qui ne fige **pas** le
|
||
qualificatif en dur — un renommage `QC→…` mord ici, pas seulement via l'INTROUVABLE du
|
||
bloc persona) et on exige de **chaque** fiche portant le pied de page que ses trois
|
||
tokens == CLAUDE.md ; une fiche avec « interaction OTOIA » mais pied de page malformé
|
||
échoue, et la **disparition totale** du recensement échoue aussi (régression #6).
|
||
**6 morsures vérifiées** : CLAUDE.md `multilingual_v2→v3` (mord les 11 fiches +, en
|
||
cascade, le bloc persona amont — l'ancre est vive) · CLAUDE.md qualif `QC→BC` (mord les
|
||
11 pieds de page, bloc indépendant) · fiche `seo` voix `v2→v1` · fiche `crm` persona
|
||
`Amélie→Sophie` · fiche `bim` pied de page malformé (parenthèses retirées) · pied de
|
||
page retiré de **toutes** les fiches (surface évaporée) ; restauré = vert.
|
||
|
||
**SDK majeur Expo de l'app mobile ancré sur la roadmap Sprint 5 l.56.** La version
|
||
**majeure** du SDK Expo que l'app compagnon `mobile/app_config` cible (`expoSdkMajor`)
|
||
est une donnée **data-derived** : `out/app_config.json` (byte-gaté ⇒ faisant autorité)
|
||
porte `expo.extra.expoSdkMajor = 54`, recopié dans `out/MANIFEST.json`
|
||
(`app.expo_sdk_major`), lui-même dérivé de `mobile_spec.json[app].expo_sdk_major` dont
|
||
la **source faisant autorité** est la roadmap Sprint 5 l.56 « **Rebuild Expo 54** » (le
|
||
spec le **déclare** via `app.expo_sdk_source`). Le README recopie ce « Expo 54 » **cinq
|
||
fois** à la main — dont la ligne de source « Expo SDK **54** | roadmap Sprint 5 l.56 ».
|
||
Le bloc **« Mobile · récap de l'app Expo »** amont ne gate que le quadruplet
|
||
onglets/rôles/langues/identifiants `a_confirmer` (comptes de `MANIFEST.counts`) —
|
||
**aveugle** au majeur du SDK. Aucun gate ne comparait cette valeur à l'artefact ni ne
|
||
l'ancrait à la roadmap : **bumper** le SDK (Expo 55 sort · `eas build` cible 55) dans le
|
||
spec/artefact **sans** toucher au README (ou l'inverse), ou **dériver la roadmap** de
|
||
l'artefact, laisse la prose **périmée en silence** pendant que le graphe byte-gaté dit
|
||
autre chose — l'agent Mobile lancerait `eas build` sur le **mauvais SDK** (le rebuild
|
||
même que la ligne roadmap prescrit) — « vert trompeur » qu'aucune suite `tests/` (qui
|
||
teste des **fonctions** de génération, jamais la prose ni l'ancre à la roadmap)
|
||
n'attrape ; même patron que l'ancrage §Entités/CLAUDE.md (marque SEO) ou les
|
||
`roadmap_line` de la recette. On **re-dérive** le majeur de **son** artefact byte-gaté
|
||
(zéro duplication du contrat #6) et on exige : **(a)** cohérence interne artefact ⇔
|
||
`MANIFEST.app.expo_sdk_major` ⇔ `spec.app.expo_sdk_major` ; **(b)** **ancrage roadmap**
|
||
— la roadmap dit « Rebuild Expo `<N>` » avec `N` == artefact (**ancre vive**) ; **(c)**
|
||
le spec **déclare** son ancrage (`expo_sdk_source` cite roadmap + Expo N) ; **(d)**
|
||
**toute** mention prose « Expo [SDK] N » == artefact (aucune périmée) ; **(e)** la ligne
|
||
de source du tableau **cite** l'ancre `roadmap` à côté du majeur. **7 morsures
|
||
vérifiées** : README `Expo SDK **54**→**55**` (prose périmée) · **artefact** `54→55`
|
||
(README + roadmap périmés — le vrai *silent green*) · **roadmap** `Rebuild Expo 54→55`
|
||
(artefact périmé · ancre qui dérive) · **MANIFEST** `54→55` (chaîne interne incohérente)
|
||
· spec `expo_sdk_source` sans « roadmap » (ancrage déclaré manquant) · README ligne de
|
||
source sans « roadmap » (ancrage prose manquant) · README retire **toute** mention
|
||
« Expo N » (claim absent) ; restauré = vert · **7 gates re-verts**.
|
||
|
||
**Compte canonique de rôles RBAC ancré sur CLAUDE.md §Architecture cible.** Le
|
||
mandat **fixe** « **RBAC 50 rôles** » (CLAUDE.md §Architecture cible). Ce nombre
|
||
canonique est recopié — **sans jamais être re-dérivé du mandat** — dans le contrat
|
||
`rbac_50_roles.json` (le champ **déclaré** `cible_rbac_roles` ET la **taille réelle**
|
||
du catalogue `len(roles)`), dans les schémas (`const: 50` de `fixtures.schema.json` /
|
||
`userperm.schema.json`) et dans la fiche agent `03_agents/erpnext_backend/AGENT.md`
|
||
(« RBAC 50 rôles natif » · « **50 rôles** » · « RBAC **50 rôles** exactement » · « RBAC
|
||
50 rôles · L38 »). Les gates RBAC amont (roleprofile/apply_plan/userperm) ne prouvent
|
||
que la **cohérence INTERNE** — chaque README de volet == son `MANIFEST`, lui-même
|
||
recompté du contrat — **aveugles** à l'ancrage au **mandat** : changer « RBAC 50 rôles »
|
||
dans CLAUDE.md, ou faire dévier le catalogue de 50 de façon **interne-cohérente**
|
||
(`cible_rbac_roles` ET `len(roles)` alignés sur une autre valeur, tous les MANIFEST
|
||
recomptés, generator vert), laisserait le contrat et la fiche affirmer « 50 exactement »
|
||
en **contredisant le mandat**, en silence — même « vert trompeur » que les tokens
|
||
branding #4 ou la persona OTOIA §Architecture cible, qu'aucune suite `tests/` (FONCTIONS
|
||
de génération, jamais l'ancre à CLAUDE.md) n'attrape. On **re-dérive** `N` de CLAUDE.md
|
||
§Architecture cible (source unique · bloc entre « ## Architecture cible » et le `##`
|
||
suivant) et on exige : **(a)** contrat `cible_rbac_roles` == N ; **(b)** taille réelle
|
||
`len(roles)` == N ; **(c)** cible == réel (le mandat est **atteint** par le catalogue) ;
|
||
**(d)** la fiche porte l'invariant emphatique « RBAC **N rôles** exactement » ET
|
||
**aucune** mention « M rôles » périmée (M != N · « RBAC 60 **tests** » n'est PAS une
|
||
mention de rôles). **5 morsures vérifiées** : CLAUDE.md « RBAC 50→60 rôles » (**ancre
|
||
vive** — mord a+b sur le contrat resté 50 ET la fiche en cascade) · contrat
|
||
`cible_rbac_roles` 50→49 (déviation déclarée · mord a+c) · contrat **retrait d'un rôle**
|
||
(`len` 50→49 · cible reste 50 · mord b+c) · fiche retire l'invariant « exactement »
|
||
(claim absent = régression #6) · fiche mention 50→48 (mention périmée) ; restauré = vert
|
||
(6 checks · exit 0) · working tree **byte-restauré** (`git checkout --`,
|
||
**JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**Comptes du run-book d'application (`RBAC_50_ROLES_SPEC.md` §7).** La NOTE §7
|
||
(« ✅ … est **livré** en-repo ») restitue, sur une **surface DISTINCTE** des READMEs
|
||
`userperm_gen` / `roleprofile_gen` (déjà gatés) et du §3 (ventilation par portail,
|
||
bloc `1quater`), **trois comptes data-derived** du run-book d'application : « **28**
|
||
templates `Company` (portée `entite`) », « **6 profils** (**5** portails métier +
|
||
console technique `plateforme`) » et « **50 rôles de façon bijective** ». Ces nombres
|
||
sont **recopiés à la main** dans la SPEC et n'étaient couverts par **aucun** gate sur
|
||
cette surface. On les **recompute** depuis le contrat `rbac_50_roles.json` (source de
|
||
vérité byte-gatée par `check_artifacts`) : **28** == |`scope_donnees == entite`| (=
|
||
nombre de `User Permission` templates), **6** == |portails distincts| (1 `Role Profile`
|
||
par portail), **5** == portails **métier** (hors `plateforme`, la console technique),
|
||
**50** == |rôles| (bijection). **Bumper** une portée `entite`→`groupe` dans le contrat
|
||
(le décompte de templates change) ou **ajouter/retirer** un portail (le décompte de
|
||
profils change) périme la note **en silence** pendant que les READMEs de volet restent
|
||
verts — le vrai « vert trompeur » (bloc `1quinquies`). Le blockquote markdown `> ` de
|
||
la note survit au collapse de `read_norm` et fragmente la prose ; il est **neutralisé**
|
||
avant match. **6 morsures vérifiées** : SPEC `28→27` (prose périmée) · SPEC
|
||
`6 profils→7` · SPEC `5 portails métier→4` · SPEC `50→49` · **contrat** une portée
|
||
`entite→groupe` (28→27 · README stale = *silent green*) · SPEC retire « 28 templates »
|
||
(claim absent = INTROUVABLE) ; restauré = vert (exit 0) · working tree **byte-restauré**
|
||
(`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**Colonne « Entité » de la §4 ancrée sur CLAUDE.md §Entités (`RBAC_50_ROLES_SPEC.md`
|
||
§4).** La table §4 « Cartographie rôles ↔ entités » liste, en **colonne de tête**, les
|
||
entités porteuses (`entite_principale`) du RBAC : les **7 entités NOMMÉES** du mandat
|
||
(`WAF` · `WA SRL` · `AC Arias Cuevas` · `Consortium ECR DR` · `Helios RD` · `Ploutos` ·
|
||
`9060 QC`) + le **périmètre consolidé** « Groupe ». Cette colonne a **deux sources
|
||
faisant autorité** — (i) **CLAUDE.md §Entités** (l'orthographe + le descripteur
|
||
parenthétique de chaque entité nommée) et (ii) `rbac_50_roles.json` (l'**ENSEMBLE** des
|
||
`entite_principale` réellement portées par les 50 rôles) — mais **aucun** gate ne la
|
||
touchait : le §3 gate les comptes par **portail** (`1quater`), le §7 les comptes du
|
||
run-book (`1quinquies`), les blocs fiches l'`entite` d'**UN** rôle ; tous **aveugles à
|
||
l'identité de CETTE liste**. On la couvre (bloc `1sexies`) : **(a) ENSEMBLE** des
|
||
entités de §4 == distinct `entite_principale` du contrat (set-diff : ni fantôme ni
|
||
manquante) ; **(b) ANCRAGE** — chaque entité NOMMÉE de CLAUDE.md §Entités est une ligne
|
||
de §4 avec son **descripteur parenthétique EXACT** (absence comprise) ; **(c)** chaque
|
||
entité nommée du mandat est **portée ≥ 1×** par le contrat (`cm_ents ⊆ contrat`, aucune
|
||
entité décorative). **AJOUTER** au contrat un rôle porté par une entité absente de §4,
|
||
**RENOMMER** une entité dans CLAUDE.md §Entités (`9060 QC→9061 QC`) ou **DÉRIVER** un
|
||
descripteur laisse la SPEC contredire le mandat/le contrat **en silence** — même « vert
|
||
trompeur » que les entités CONFOTUR (§Entités) ou la marque SEO org. **8 morsures
|
||
vérifiées** : **contrat** ajoute un rôle porté par une entité absente de §4 (`(a)`
|
||
manquante) · **contrat** renomme toutes les entités `Ploutos→Ploutos SRL` (`(a)` fantôme
|
||
`Ploutos` + manquante `Ploutos SRL` · `(c)` `Ploutos` décorative) · **CLAUDE.md**
|
||
`9060 QC→9061 QC` (`(b)` absent + `(c)` décorative) · **CLAUDE.md** descripteur
|
||
`(holding)→(holding co)` (`(b)` mismatch) · **SPEC §4** `AC Arias Cuevas→AC Arias`
|
||
(`(a)` fantôme+manquante + `(b)` absent) · **SPEC §4** descripteur `marque publique→
|
||
marque privée` (`(b)` mismatch) · **SPEC §4** retire la ligne « Groupe » (`(a)`
|
||
manquante) · **SPEC** supprime l'en-tête `## 4.` (section INTROUVABLE) ; restauré = vert
|
||
(exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) ·
|
||
**7 gates re-verts**.
|
||
|
||
**Ensemble des portails métier + exclusion nommée de la console technique
|
||
(`03_agents/frontend_console/AGENT.md`).** Le bloc « triplets par workspace » recompute
|
||
cartes/liens/rôles **par** portail mais reste **aveugle à QUELS portails** composent
|
||
l'ensemble. La fiche l'énumère à la main en **deux endroits** data-derived : L11
|
||
« **5 portails rôle métier** (Ventes / Construction / Achat / Compta / Direction) » et
|
||
L46 « la console technique `plateforme` (**6ᵉ portail RBAC**) est **exclue** ». On les
|
||
couvre (bloc « Portails ensemble ») : **(a)** l'**ensemble ORDONNÉ des noms courts** ==
|
||
les workspaces métier de `frontend/portails/out/MANIFEST.json` byte-gaté (nom court =
|
||
label « OTO <Nom> » sans le préfixe, comme l'énumération des onglets Mobile) **ET, DOUBLE
|
||
ANCRAGE**, leurs ids == `portails_business` du **contrat RBAC** (l'« égalité stricte »
|
||
que la fiche revendique elle-même en L47) ; **(b)** `plateforme` == l'**UNIQUE** portail
|
||
RBAC **hors métier** (∈ portails distincts des rôles ∖ `portails_business`), et c'est le
|
||
**6ᵉ** (total distinct == 6 = 5 métier + 1). Un **ÉCHANGE** (Achat→Marketing), un
|
||
**RETRAIT**, un **RÉORDRE**, un **RENOMMAGE** de la console technique dans le contrat
|
||
(`plateforme`→`console`) ou un contrat qui **PROMEUT** `plateforme` en métier laisse la
|
||
fiche contredire les artefacts **en silence** — même patron que l'énumération des onglets
|
||
Mobile ou la colonne « Entité » de la §4. **8 morsures vérifiées** : **fiche L11** échange
|
||
`Achat→Marketing` (`(a)` en trop/absent) · **fiche L11** retire `Compta` (`(a)` compte +
|
||
absent) · **fiche L11** réordonne `Ventes↔Construction` (`(a)` ordre) · **fiche L46**
|
||
`plateforme→console` (`(b)` nom) · **fiche L46** `6ᵉ→7ᵉ` (`(b)` rang) · **fiche L46**
|
||
retire `**exclue**` (claim INTROUVABLE) · **contrat** réordonne `portails_business`
|
||
(ancrage rompu) · **contrat** renomme `ventes→sales` dans `portails_business` (ancrage
|
||
rompu + `plateforme` n'est plus l'unique portail hors métier) ; restauré = vert (exit 0) ·
|
||
working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · **7 gates
|
||
re-verts**.
|
||
|
||
**IDENTITÉ de l'endpoint GPU RunPod ComfyUI ancrée sur l'inventaire maître
|
||
(`03_agents/{bim,rendu}/AGENT.md` + `02_master_prompt/`).** Les fiches BIM/Rendu et le
|
||
master prompt NOMMENT, entre backticks, l'ID du pod RunPod ComfyUI qui exécute le rendu
|
||
Flux (`t68akvx80wv092`) + le tag du modèle (« Flux dev fp8 ») — **6 citations SAISIES à
|
||
la main** dont la source de vérité in-repo est le bullet §1 de `AGENTS_EXISTING_ASSETS.md`
|
||
(« le fichier maître » que les fiches citent explicitement comme leur ancre), **hors de
|
||
tout gate** : `check_docs` ne voit qu'un lien, `guard_constraints` qu'un terme interdit,
|
||
`check_artifacts` qu'un `out/` — un ID de pod n'est aucun des trois. Or un pod RunPod est
|
||
**RE-CRÉÉ avec un NOUVEL id** (le cas le plus courant) : l'inventaire se met à jour, les
|
||
citations se **PÉRIMENT en silence** → l'agent Rendu/BIM câblerait le batch Flux vers un
|
||
**endpoint GPU MORT** (l'invention même que #6 interdit). On RECOMPUTE l'id + le tag depuis
|
||
le bullet maître (jamais une liste à la main) ; **direction fiche→inventaire** : toute
|
||
fiche/master prompt qui **mentionne RunPod** doit citer l'id **EXACT** (tout token backtické
|
||
de forme pod — alphanum minuscule ≥12 avec un chiffre — == canonique) **et** le tag Flux
|
||
**EXACT** ; la disparition **totale** de la surface échoue AUSSI. **6 morsures vérifiées** :
|
||
**inventaire** re-crée le pod (id change → mord les **3 fiches** en cascade, id périmé) ·
|
||
**inventaire** bump le tag Flux (`dev fp8→schnell fp8` → tag périmé dans les 3) · **fiche
|
||
rendu** mistype une citation (id fantôme) · **fiche bim** retire le tag Flux (tag absent) ·
|
||
**master prompt** retire l'id backtické (citation incomplète) · **inventaire** supprime le
|
||
bullet canonique (ancre morte, recompute impossible) ; restauré = vert (exit 0) · working
|
||
tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) · **7 gates re-verts**.
|
||
|
||
**IDENTITÉ des évènements d'émission e-CF (le « cœur du livrable » fiscal) ancrée sur
|
||
`ecf_plan.json`.** La section §Cross-cohérence de `fiscal/ecf_dgii/README.md:40-59` — « le
|
||
cœur du livrable » — transcrit **à la main** l'identité des `emission_events` de l'artefact
|
||
byte-gaté : l'**ENSEMBLE** des évènements qui émettent un e-CF (« seulement réservation et
|
||
contrat »), l'**ENSEMBLE exclu** des brouillons (« on ne facture pas un brouillon
|
||
(lead/visite/devis) »), l'ENSEMBLE des `base_field` (`montant_reservation`/`montant_contrat`),
|
||
le **rôle unique** (`compta-fiscaliste-ecf` · « OTO Compta Fiscaliste eCF » · portail compta)
|
||
et la **FormaPago défaut** (« = 3 (Tarjeta) » · Cardnet #10). Le seul gate fiscal existant
|
||
couvre le **format e-NCF** + le compte de tests ; le bloc racine, le seul agrégat « e-CF 39
|
||
tests ». **Rien** ne confrontait cette identité à l'artefact. Dérive la plus grave (#6/#5) :
|
||
**ajouter un `emission_event` sur un état BROUILLON** (ex. `devis`) — l'anti-pattern EXACT
|
||
que le module interdit (« on ne facture pas un brouillon ») — pendant que le README dit encore
|
||
« seulement réservation et contrat » → l'agent ERPNext Backend câblerait une **facture émise
|
||
avant soumission/encaissement**. On RECOMPUTE les ENSEMBLES depuis `ecf_plan.json` (jamais une
|
||
liste à la main) et exige que le README les reflète **EXACTEMENT** (set-diff : ni fantôme ni
|
||
manquant ; accents/casse normalisés) ; le rôle est **double-ancré** sur `rbac_50_roles.json`
|
||
(id → portail compta → nom ERPNext concordant) ; l'ensemble exclu doit rester **DISJOINT** des
|
||
émetteurs (jamais de facture sur brouillon). Cohérences croisées en bonus : rôle **unique** sur
|
||
tous les évènements · défaut ∈ **catalogue** FormaPago · ensembles non vides. **6 morsures
|
||
vérifiées** : artefact facture un brouillon (`devis` émetteur → mismatch d'ensemble **ET**
|
||
clash de disjonction) · artefact renomme un `base_field` · artefact change la FormaPago défaut
|
||
`3→1` · artefact casse le rôle unique (2ᵉ évènement → autre rôle) · README périmé « seulement
|
||
contrat » (réservation retiré) · motif du défaut ne cite plus « Cardnet » (#10) ; restauré =
|
||
vert (exit 0) · working tree **byte-restauré** (`git checkout --`, **JAMAIS** `git clean`) ·
|
||
**7 gates re-verts**.
|
||
|
||
**COMPLÉTUDE de l'énumération des gates statiques dans la fiche QA.** La fiche
|
||
`03_agents/qa/AGENT.md` (§« Deuxième étage QA ») énumère les gates `ci/*.sh` en se
|
||
**déclarant explicitement** « l'état courant » de `git ls-files 'ci/*.sh'` (« pas une
|
||
constante »). Or **rien** ne vérifiait que CHAQUE gate statique y figure vraiment : le 8ᵉ
|
||
gate `check_mobile_workflow.sh` (livré le 2026-08-05) a pu rester **omis un jour entier EN
|
||
SILENCE** — la fiche promettait la complétude sans la tenir. On **re-dérive** l'ensemble
|
||
depuis `git ls-files 'ci/*.sh'` (hors `lib.sh`, #6 — jamais une liste à la main) et on
|
||
exige que chaque basename apparaisse (backtické) dans la fiche ; un gate **absent** échoue.
|
||
Distinct de `check_ci_integrity` qui verrouille le **câblage CI** (chaque `ci/*.sh` ↔ un
|
||
job `gate.needs`) mais **ignore la fiche** ; cette surface ferme le miroir *documentaire* du
|
||
même ensemble. **2 morsures vérifiées** : retrait de la ligne `check_mobile_workflow.sh` de
|
||
la fiche → mismatch (gate réel non cité) · ajout d'un 9ᵉ `ci/*.sh` tracké non encore
|
||
documenté → mismatch (**forward-protection** : le prochain gate ne pourra pas être omis) ;
|
||
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.
|
||
|
||
### `check_mobile_workflow.sh` — intégrité du second workflow `mobile-build.yml`
|
||
`mobile-build.yml` (EAS build iOS/Android · Sprint 5) est **hors `gate.needs`** :
|
||
aucun de ses jobs ne conditionne un merge (ils s'activent seulement quand Michel
|
||
fournit `EAS_TOKEN`). Conséquence longtemps tolérée : **aucun gate ne lintait sa
|
||
structure** — `check_ci_integrity.sh` ne verrouille que `ci.yml`, `validate_json.sh`
|
||
que le `.json`. Mutation vérifiée : réintroduire `secrets.` dans un `if:` de job y
|
||
passait **7/7 gates statiques verts**. Or ce fichier porte un correctif de
|
||
portabilité subtil (session `234104`) qui, s'il régresse, ne casse rien au CI mais
|
||
fait échouer les builds « le jour où on en a besoin ». Ce gate rend 4 invariants
|
||
opposables, tous **lus dans le fichier** (jamais une liste à la main · #6) :
|
||
|
||
- **MOB-1 · bien-formé** — le fichier existe, porte `name:`/`on:`/`jobs:` et les
|
||
3 jobs attendus (`preflight`, `build-ios`, `build-android`). Parse YAML strict
|
||
**exigé si PyYAML est présent**, sinon contrôle awk seul (runner sans pip →
|
||
best-effort ; jamais un faux vert, car MOB-2/3/4 ne dépendent pas de PyYAML).
|
||
- **MOB-2 · portabilité du gating** — **0** `secrets.` dans un `if:` au niveau
|
||
**job** (`^ if:`). Le contexte `secrets` n'est **pas** exposé dans
|
||
`jobs.<id>.if` (table d'availability GitHub Actions) ; sur `act`/Gitea il
|
||
s'évaluerait vide → job **SKIP silencieux** même token fourni. Exactement la
|
||
régression corrigée en `234104` (passage au pattern preflight-output).
|
||
- **MOB-3 · activation différée préservée (#6/#8)** — chaque job de **build**
|
||
(bloc lançant `run: eas build`) porte un `if:` job-level gaté sur
|
||
`needs.preflight.outputs.has_token` → SKIP tant que le token absent, donc le
|
||
workflow ne rend **jamais** le CI rouge avant activation Michel.
|
||
- **MOB-4 · contrat d'outputs intact** — `preflight` déclare `has_token`/`has_repo`,
|
||
chaque `needs.preflight.outputs.<X>` référencé est bien **déclaré** (0 dangling →
|
||
pas de SKIP par output fantôme) et chaque output déclaré est **alimenté** par un
|
||
`echo "<X>=… >> $GITHUB_OUTPUT"`.
|
||
|
||
Les 5 mutations correspondantes (YAML cassé · `secrets.` en `if:` de job · `if:`
|
||
de build supprimé · output fantôme référencé · output non alimenté) rendent toutes
|
||
le gate **ROUGE** ; le fichier propre repasse **vert**. stdlib + awk pur, PyYAML
|
||
best-effort, zéro réseau. Ce gate est le **8ᵉ gate statique** `ci/*.sh` (câblé dans
|
||
`gate.needs`, donc rejoué par `run_ci.sh`).
|
||
|
||
### `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 gates statiques `ci/*.sh` 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.
|