[DTP-Worker] Sprint 8 · Générateur Matrice d'acceptation / traçabilité MVP (15 promesses roadmap · 21 modules gated tracés bijectif · partition sprint exacte · 12 hors-périmètre sourcés) (QA · roadmap L75/L80-87)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-1
@@ -405,10 +405,30 @@ jobs:
|
|||||||
- name: Tests run-book de déploiement
|
- name: Tests run-book de déploiement
|
||||||
run: python3 -m unittest discover -s tests -v
|
run: python3 -m unittest discover -s tests -v
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Matrice d'acceptation / traçabilité MVP (QA · Sprint 8 · buffer L75). Document
|
||||||
|
# de RECETTE de méta-niveau : mappe chaque promesse roadmap (8 livrables de
|
||||||
|
# sprint + 7 métriques succès MVP) vers sa preuve gated OU un hors-périmètre
|
||||||
|
# sourcé. Périmètre PROUVÉ : modules-preuve dérivés du CI (parse_ci · #5) et
|
||||||
|
# confrontés de façon BIJECTIVE à la réalité ; fenêtres de sprint lues dans le
|
||||||
|
# registre 4Big (anti-dérive). Anti-invention (#6) : les nombres des énoncés
|
||||||
|
# sont des citations verbatim de la roadmap. stdlib pur · zéro pip.
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
qa-acceptance-tests:
|
||||||
|
name: QA · Matrice d'acceptation / traçabilité MVP (Sprint 8)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: 05_deliverables_mvp/qa/acceptance
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Tests matrice d'acceptation
|
||||||
|
run: python3 -m unittest discover -s tests -v
|
||||||
|
|
||||||
gate:
|
gate:
|
||||||
name: Gate qualité (agrégat)
|
name: Gate qualité (agrégat)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests]
|
needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests]
|
||||||
steps:
|
steps:
|
||||||
- name: Résultat
|
- name: Résultat
|
||||||
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
||||||
|
|||||||
@@ -1,5 +1,70 @@
|
|||||||
# Activity Log · 2026-07-30 · Claude Code DTP
|
# Activity Log · 2026-07-30 · Claude Code DTP
|
||||||
|
|
||||||
|
## Session `20260730_115744` (session 24)
|
||||||
|
|
||||||
|
**Tâche** : **Sprint 8 · QA (buffer L75 · recette)** — Générateur de la **Matrice
|
||||||
|
d'acceptation / traçabilité MVP**. Sprint 8 étant le dernier (déploiement VPS réel
|
||||||
|
hors périmètre worker · #8), le volet réalisable en-repo restant est la
|
||||||
|
**recette** : mapper chaque **promesse** roadmap (8 livrables de sprint L33..L76 +
|
||||||
|
7 métriques succès MVP L81..L87) vers sa **preuve** livrée OU un hors-périmètre
|
||||||
|
worker sourcé. Consolidation naturelle du « DELIVERABLE MVP » (L76) + métriques
|
||||||
|
succès (L80-87).
|
||||||
|
|
||||||
|
**Gap comblé** : aucun artefact ne traçait les promesses roadmap → livrables ; les
|
||||||
|
preuves étaient éparpillées (gap analysis + 23 rapports) sans vue bijective ni
|
||||||
|
recensement des parties hors périmètre worker.
|
||||||
|
|
||||||
|
**Décision d'architecture** : harnais de **MÉTA-NIVEAU** orienté RECETTE, **axe
|
||||||
|
distinct** des 4 autres méta (non redondant · #5) — `audit_4big` note la qualité
|
||||||
|
statique · `regression` prouve l'exécution · `deploy_runbook` ordonne le
|
||||||
|
déploiement · **`acceptance`** prouve la **couverture des promesses** roadmap.
|
||||||
|
|
||||||
|
**Anti-invention (cœur · #6)** : périmètre **PROUVÉ** — modules-preuve **dérivés
|
||||||
|
du CI** (`q4lib.registry.parse_ci`, réutilisé · zéro duplication) et confrontés de
|
||||||
|
façon **BIJECTIVE** (module gated non tracé OU preuve non gated → refus ; la
|
||||||
|
validation **recalcule** depuis le CI) ; **cross-cohérence** : fenêtre de sprint
|
||||||
|
**lue** dans le registre 4Big (anti-dérive), l'auditeur (SoD) via
|
||||||
|
`extra_module_sprint` sourcé ; **partition exacte** par sprint ; **zéro chiffre
|
||||||
|
fabriqué** (les nombres des énoncés `<1h`/`95/100`/`7 dashboards`/`2 stores` sont
|
||||||
|
des **citations verbatim** de la roadmap) ; tout hors-périmètre **sourcé** ;
|
||||||
|
**SoD** : la matrice ne se cite jamais elle-même.
|
||||||
|
|
||||||
|
**Fichiers créés** — `05_deliverables_mvp/qa/acceptance/` :
|
||||||
|
- `acceptance_spec.json` (8 livrables + 7 métriques · `roadmap_line` · hors-
|
||||||
|
périmètre sourcé · **0 chiffre fabriqué**)
|
||||||
|
- `acclib/{__init__,deps,builder}.py` (`deps` réutilise `parse_ci` + validateur
|
||||||
|
Publiciste + registre 4Big ; `builder` pur/déterministe)
|
||||||
|
- `acceptance_gen.py` (CLI `build`/`validate` · **10 familles d'invariants**)
|
||||||
|
- `acceptance.schema.json` (contrat draft-07)
|
||||||
|
- `out/{acceptance_matrix,MANIFEST}.json` (hand-off · verdict) ·
|
||||||
|
`tests/test_acceptance.py` (**31 tests** dont 14 injections négatives) ·
|
||||||
|
`README.md` · `.gitignore`
|
||||||
|
|
||||||
|
**Fichiers modifiés** :
|
||||||
|
- `.gitea/workflows/ci.yml` : job `qa-acceptance-tests` + ajout au `gate`.
|
||||||
|
- `qa/audit_4big/` : enregistrement du module (couverture bijective **20 → 21** ·
|
||||||
|
PASS 21/21 à min 100) ; `out/` régénéré.
|
||||||
|
- `devops/deploy_runbook/` : `module_phase` +`qa/acceptance` (`verification-qa`) ;
|
||||||
|
bijectif **20 → 21** ; `out/` régénéré.
|
||||||
|
- `qa/regression/out/` : plan régénéré (**20 → 21 suites** · découverte auto CI).
|
||||||
|
|
||||||
|
**Résultat** : matrice `verdict=true` — **15 promesses** (8 sprint + 7 métriques) ·
|
||||||
|
**21 modules gated tracés sans doublon** (bijectif) · **partition par sprint
|
||||||
|
exacte** · **12 hors-périmètre sourcés**.
|
||||||
|
|
||||||
|
**Vérifs** : 31/31 tests module ; validations méta vertes ; régression `run`
|
||||||
|
exhaustive **21/21 suites vertes · 534 tests passés · 0 échec · 0 erreur**
|
||||||
|
(503 → +31) ; gate CI local vert (guard + JSON + docs + YAML) ; build déterministe.
|
||||||
|
|
||||||
|
**Hors périmètre worker (VPS · #8)** : le module ne déploie rien — exécution réelle
|
||||||
|
(démo publique, run-book VPS, builds stores, indexation, voix Amélie) → agent
|
||||||
|
DevOps / direction.
|
||||||
|
|
||||||
|
**Détail complet** : voir
|
||||||
|
[`05_deliverables_mvp/daily_reports/2026-07-30-session24.md`](../05_deliverables_mvp/daily_reports/2026-07-30-session24.md).
|
||||||
|
|
||||||
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|
||||||
## Session `20260730_112734` (session 23)
|
## Session `20260730_112734` (session 23)
|
||||||
|
|
||||||
**Tâche** : **Sprint 8 · DevOps** — Générateur du **Run-book de déploiement VPS
|
**Tâche** : **Sprint 8 · DevOps** — Générateur du **Run-book de déploiement VPS
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Rapport de session · 2026-07-30 · session 24
|
||||||
|
|
||||||
|
## Tâche
|
||||||
|
**Sprint 8 · QA (buffer L75 · recette)** — Générateur de la **Matrice
|
||||||
|
d'acceptation / traçabilité MVP** : mappe chaque **promesse** de la roadmap
|
||||||
|
(8 livrables de sprint L33..L76 + 7 métriques succès MVP L81..L87) vers sa
|
||||||
|
**preuve** livrée (module gated du CI) OU un **hors-périmètre worker sourcé**.
|
||||||
|
|
||||||
|
Sprint 8 étant le dernier (déploiement VPS réel + monitoring hors périmètre
|
||||||
|
worker · #8), le volet réalisable en-repo restant est la **recette** : prouver
|
||||||
|
que le MVP promis est intégralement honoré et que ce qui reste est explicitement
|
||||||
|
hors périmètre. C'est la consolidation naturelle du « DELIVERABLE MVP » (L76) et
|
||||||
|
des métriques succès (L80-87).
|
||||||
|
|
||||||
|
## Gap comblé
|
||||||
|
Aucun artefact ne traçait les **promesses roadmap** vers les livrables. Les
|
||||||
|
preuves étaient éparpillées (gap analysis Sprint 1 + 23 rapports de session) sans
|
||||||
|
vue bijective « promesse → preuve », ni recensement des parties **hors périmètre
|
||||||
|
worker** (builds stores, indexation runtime, voix Amélie, déploiement VPS).
|
||||||
|
|
||||||
|
## Décision d'architecture
|
||||||
|
Harnais de **MÉTA-NIVEAU** orienté RECETTE, **axe distinct** des 4 autres méta
|
||||||
|
(non redondant · #5) :
|
||||||
|
|
||||||
|
| Harnais | Axe prouvé |
|
||||||
|
|---|---|
|
||||||
|
| `qa/audit_4big` | qualité **statique** par module (95+/100) |
|
||||||
|
| `qa/regression` | chaque suite **s'exécute** au vert |
|
||||||
|
| `devops/deploy_runbook` | **ordre** de déploiement VPS |
|
||||||
|
| **`qa/acceptance`** (nouveau) | **couverture des promesses** roadmap |
|
||||||
|
|
||||||
|
## Anti-invention (cœur · #6)
|
||||||
|
- **Périmètre PROUVÉ** : les modules-preuve sont **dérivés du CI**
|
||||||
|
(`q4lib.registry.parse_ci`, réutilisé · zéro duplication · #5) et confrontés de
|
||||||
|
façon **BIJECTIVE** — un module gated non tracé OU une preuve non gated →
|
||||||
|
génération **refusée** ; la validation **recalcule** la couverture depuis le CI.
|
||||||
|
- **Cross-cohérence** : la fenêtre de sprint de chaque module est **lue** dans le
|
||||||
|
registre de l'auditeur 4Big (`quality_spec.json`), jamais re-déclarée
|
||||||
|
(anti-dérive) ; l'auditeur lui-même (SoD · absent de son registre) reçoit sa
|
||||||
|
fenêtre via `extra_module_sprint`, **avec source**.
|
||||||
|
- **Partition exacte** par sprint : chaque livrable SX cite exactement les modules
|
||||||
|
gated de fenêtre SX (ni trou ni chevauchement).
|
||||||
|
- **Zéro chiffre fabriqué** : les nombres des énoncés (`<1h`, `95/100`, `7
|
||||||
|
dashboards`, `2 stores`) sont des **citations verbatim** de la roadmap ; la
|
||||||
|
matrice ne prétend **aucune** mesure de performance.
|
||||||
|
- Tout **hors-périmètre** porte une **source** (#8 VPS · builds stores · runtime) ;
|
||||||
|
**SoD** : la matrice ne se cite jamais elle-même.
|
||||||
|
|
||||||
|
## Fichiers créés — `05_deliverables_mvp/qa/acceptance/`
|
||||||
|
- `acceptance_spec.json` (8 livrables de sprint + 7 métriques MVP · `roadmap_line`
|
||||||
|
pour chacun · hors-périmètre sourcé · **0 chiffre fabriqué**)
|
||||||
|
- `acclib/{__init__,deps,builder}.py` (`deps` réutilise `parse_ci` + validateur
|
||||||
|
Publiciste + registre 4Big ; `builder` pur/déterministe)
|
||||||
|
- `acceptance_gen.py` (CLI `build`/`validate` · **10 familles d'invariants**)
|
||||||
|
- `acceptance.schema.json` (contrat draft-07)
|
||||||
|
- `out/{acceptance_matrix,MANIFEST}.json` (hand-off · verdict) ·
|
||||||
|
`tests/test_acceptance.py` (**31 tests** dont 14 injections négatives) ·
|
||||||
|
`README.md` · `.gitignore`
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
- `.gitea/workflows/ci.yml` : job `qa-acceptance-tests` + ajout au `gate`.
|
||||||
|
- `qa/audit_4big/` : enregistrement du module (couverture bijective **20 → 21** ·
|
||||||
|
PASS 21/21 à min 100) ; `out/` régénéré.
|
||||||
|
- `devops/deploy_runbook/` : `module_phase` +`qa/acceptance` (phase
|
||||||
|
`verification-qa`) ; couverture bijective **20 → 21** ; `out/` régénéré.
|
||||||
|
- `qa/regression/out/` : plan régénéré (**20 → 21 suites** · découverte auto CI).
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
Matrice `verdict=true` — **15 promesses roadmap** (8 sprint + 7 métriques) ·
|
||||||
|
**21 modules gated tracés sans doublon** (bijectif) · **partition par sprint
|
||||||
|
exacte** (S2=7, S3=1, S4=5, S5=2, S6=2, S7=2, S8=2 ; S1 sur artefact) ·
|
||||||
|
**12 hors-périmètre sourcés**.
|
||||||
|
|
||||||
|
## Vérifs
|
||||||
|
- 31/31 tests module ; validations méta vertes (audit_4big 21/21 min 100 ·
|
||||||
|
deploy 21/21 bijectif · regression 21 suites · acceptance bijectif+partition).
|
||||||
|
- Régression `run` exhaustive : **21/21 suites vertes · 534 tests passés · 0
|
||||||
|
échec · 0 erreur** (503 → +31).
|
||||||
|
- Gate CI statique local vert (guard constraints · JSON · docs · YAML `ci.yml`).
|
||||||
|
- Build déterministe (régénérable bit-à-bit).
|
||||||
|
|
||||||
|
## Hors périmètre worker (VPS · #8)
|
||||||
|
Ce module **ne déploie rien** : il produit un document de recette en-repo.
|
||||||
|
L'exécution réelle (démo publique, application du run-book VPS, builds stores,
|
||||||
|
indexation, voix Amélie) revient à l'agent DevOps / la direction.
|
||||||
|
|
||||||
|
## Auto-score 4Big : 96/100
|
||||||
@@ -89,7 +89,8 @@
|
|||||||
"demo/scenarios": "contenu",
|
"demo/scenarios": "contenu",
|
||||||
"qa/audit_5d": "verification-qa",
|
"qa/audit_5d": "verification-qa",
|
||||||
"qa/audit_4big": "verification-qa",
|
"qa/audit_4big": "verification-qa",
|
||||||
"qa/regression": "verification-qa"
|
"qa/regression": "verification-qa",
|
||||||
|
"qa/acceptance": "verification-qa"
|
||||||
},
|
},
|
||||||
"confirmations": {
|
"confirmations": {
|
||||||
"custom_modules": {
|
"custom_modules": {
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
"self_module": "devops/deploy_runbook",
|
"self_module": "devops/deploy_runbook",
|
||||||
"counts": {
|
"counts": {
|
||||||
"phases": 7,
|
"phases": 7,
|
||||||
"modules": 20,
|
"modules": 21,
|
||||||
"modules_mapped": 20,
|
"modules_mapped": 21,
|
||||||
"confirmations": 7,
|
"confirmations": 7,
|
||||||
"confirmations_referenced": 7
|
"confirmations_referenced": 7
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"ci_modules_count": 20,
|
"ci_modules_count": 21,
|
||||||
"mapped_modules_count": 20,
|
"mapped_modules_count": 21,
|
||||||
"bijective": true,
|
"bijective": true,
|
||||||
"missing_in_map": [],
|
"missing_in_map": [],
|
||||||
"extra_in_map": [],
|
"extra_in_map": [],
|
||||||
|
|||||||
@@ -203,6 +203,10 @@
|
|||||||
"contenu"
|
"contenu"
|
||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "qa/acceptance",
|
||||||
|
"ci_job": "qa-acceptance-tests"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "qa/audit_4big",
|
"module": "qa/audit_4big",
|
||||||
"ci_job": "qa-audit-4big-tests"
|
"ci_job": "qa-audit-4big-tests"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Matrice d'acceptation / traçabilité MVP — `qa/acceptance`
|
||||||
|
|
||||||
|
**Sprint 8 · QA (buffer L75 · corrections finales / recette).** Document de
|
||||||
|
**RECETTE** de méta-niveau : il mappe chaque **promesse** de la roadmap
|
||||||
|
(`04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`) vers sa **preuve** livrée, et prouve
|
||||||
|
qu'aucune promesse n'est silencieusement oubliée.
|
||||||
|
|
||||||
|
- **8 livrables de sprint** (L33..L76) → modules gated qui les réalisent en-repo.
|
||||||
|
- **7 métriques succès MVP** (L81..L87) → preuve gated ou hors-périmètre sourcé.
|
||||||
|
|
||||||
|
## Question de recette à laquelle il répond
|
||||||
|
|
||||||
|
> « Le MVP promis est-il honoré, et ce qui reste est-il **explicitement** hors
|
||||||
|
> périmètre worker (VPS #8 · builds stores · services runtime) ? »
|
||||||
|
|
||||||
|
## Pourquoi un module distinct (non redondant · #5)
|
||||||
|
|
||||||
|
| Harnais méta | Axe prouvé |
|
||||||
|
|---|---|
|
||||||
|
| `qa/audit_4big` | qualité **statique** de chaque module (95+/100) |
|
||||||
|
| `qa/regression` | chaque suite **s'exécute** au vert |
|
||||||
|
| `devops/deploy_runbook` | **ordre** de déploiement VPS des modules |
|
||||||
|
| **`qa/acceptance`** (ici) | **couverture des promesses** roadmap (la recette) |
|
||||||
|
|
||||||
|
## Périmètre PROUVÉ, pas déclaré (anti-invention · #6)
|
||||||
|
|
||||||
|
- L'ensemble des **modules-preuve** est **dérivé du CI** (`q4lib.registry.parse_ci`,
|
||||||
|
réutilisé · zéro duplication) et confronté de façon **BIJECTIVE** à la réalité :
|
||||||
|
un module gated **non tracé** OU une preuve **non gated** → génération **refusée**.
|
||||||
|
- La **fenêtre de sprint** de chaque module est **lue** dans le registre de
|
||||||
|
l'auditeur 4Big (`qa/audit_4big/quality_spec.json`) — jamais re-déclarée ici
|
||||||
|
(anti-dérive). L'auditeur lui-même (absent de son propre registre · SoD) reçoit
|
||||||
|
sa fenêtre via `extra_module_sprint`, **avec source**.
|
||||||
|
- **Partition exacte** par sprint : chaque livrable de sprint SX cite exactement
|
||||||
|
les modules gated de fenêtre SX (ni trou ni chevauchement).
|
||||||
|
- Les **nombres** des énoncés (`<1h`, `95/100`, `7 dashboards`, `2 stores`) sont
|
||||||
|
des **citations verbatim** de la roadmap — la matrice ne **prétend** aucune
|
||||||
|
mesure de performance.
|
||||||
|
- Tout **hors-périmètre** porte une **source** (`CLAUDE.md #8` VPS, builds stores,
|
||||||
|
services runtime) : aucune omission silencieuse.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 acceptance_gen.py build # écrit out/acceptance_matrix.json + out/MANIFEST.json
|
||||||
|
python3 acceptance_gen.py validate # (re)génère en mémoire + valide (schéma + 10 invariants)
|
||||||
|
python3 -m unittest discover -s tests -p 'test_*.py' -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sorties (`out/`)
|
||||||
|
|
||||||
|
- `acceptance_matrix.json` — une ligne par promesse : preuves modules résolues
|
||||||
|
(avec job CI), artefacts, hors-périmètre sourcé, drapeaux + **verdict** global.
|
||||||
|
- `MANIFEST.json` — comptes, preuve de **couverture bijective** vs CI, **partition
|
||||||
|
par sprint**.
|
||||||
|
|
||||||
|
Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
|
||||||
|
|
||||||
|
## Invariants (10 familles)
|
||||||
|
|
||||||
|
1. ids uniques ; livrables S1..S8 + métriques M1..M7 ordonnés.
|
||||||
|
2. toute preuve module est **gated** (drapeau recalculé depuis le CI).
|
||||||
|
3. **couverture bijective** cités ⇔ gated (hors self) — cœur anti-invention.
|
||||||
|
4. fenêtre de sprint connue pour tout module gated ; extra sans conflit registre.
|
||||||
|
5. **partition exacte** par sprint.
|
||||||
|
6. tout hors-périmètre porte une source non vide.
|
||||||
|
7. cohérence statut/preuve ; tout artefact cité **existe** sur disque.
|
||||||
|
8. `mvp_metric` ⇒ sprint nul ; `sprint_deliverable` ⇒ sprint renseigné.
|
||||||
|
9. **SoD** : la matrice ne se cite jamais elle-même comme preuve.
|
||||||
|
10. `roadmap_line` entier positif.
|
||||||
|
|
||||||
|
## Hors périmètre worker (#8)
|
||||||
|
|
||||||
|
Ce module **ne déploie rien** : il produit un document de recette en-repo.
|
||||||
|
L'application réelle (démo publique, VPS) reste à l'agent DevOps / la direction.
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://oto-enterprise-os-dtp/qa/acceptance.schema.json",
|
||||||
|
"title": "Matrice d'acceptation / traçabilité MVP (roadmap → livrable → preuve)",
|
||||||
|
"description": "Contrat de sortie de acceptance_gen.py. Validé par le validateur maison Publiciste (zéro pip) dans le job CI qa-acceptance-tests. Mappe chaque promesse roadmap (livrables de sprint + métriques succès MVP) vers sa preuve gated (module CI) ou un hors-périmètre sourcé. Aucun chiffre métier fabriqué : les nombres des énoncés sont des citations verbatim de la roadmap.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["matrix", "manifest", "verdict"],
|
||||||
|
"properties": {
|
||||||
|
"matrix": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "$ref": "#/definitions/row" }
|
||||||
|
},
|
||||||
|
"verdict": { "type": "boolean" },
|
||||||
|
"manifest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"generated_from",
|
||||||
|
"reference_cadre",
|
||||||
|
"roadmap_ref",
|
||||||
|
"cible_portage",
|
||||||
|
"self_module",
|
||||||
|
"counts",
|
||||||
|
"coverage",
|
||||||
|
"roadmap"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"generated_from": { "type": "string", "minLength": 1 },
|
||||||
|
"reference_cadre": { "type": "string", "minLength": 1 },
|
||||||
|
"roadmap_ref": { "type": "string", "minLength": 1 },
|
||||||
|
"cible_portage": { "type": "string", "minLength": 1 },
|
||||||
|
"self_module": { "type": "string", "const": "qa/acceptance" },
|
||||||
|
"counts": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"sprint_deliverables",
|
||||||
|
"mvp_metrics",
|
||||||
|
"deliverables_total",
|
||||||
|
"in_repo",
|
||||||
|
"out_of_scope_full",
|
||||||
|
"gated_modules",
|
||||||
|
"cited_modules",
|
||||||
|
"out_of_scope_items"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"sprint_deliverables": { "type": "integer", "minimum": 1 },
|
||||||
|
"mvp_metrics": { "type": "integer", "minimum": 1 },
|
||||||
|
"deliverables_total": { "type": "integer", "minimum": 1 },
|
||||||
|
"in_repo": { "type": "integer", "minimum": 0 },
|
||||||
|
"out_of_scope_full": { "type": "integer", "minimum": 0 },
|
||||||
|
"gated_modules": { "type": "integer", "minimum": 1 },
|
||||||
|
"cited_modules": { "type": "integer", "minimum": 0 },
|
||||||
|
"out_of_scope_items": { "type": "integer", "minimum": 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"ci_modules_count",
|
||||||
|
"cited_modules_count",
|
||||||
|
"bijective",
|
||||||
|
"missing_in_matrix",
|
||||||
|
"phantom_evidence",
|
||||||
|
"partition_ok",
|
||||||
|
"sprint_partition",
|
||||||
|
"unknown_sprint",
|
||||||
|
"missing_artifacts",
|
||||||
|
"self_module_excluded"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ci_modules_count": { "type": "integer", "minimum": 0 },
|
||||||
|
"cited_modules_count": { "type": "integer", "minimum": 0 },
|
||||||
|
"bijective": { "type": "boolean" },
|
||||||
|
"missing_in_matrix": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"phantom_evidence": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"partition_ok": { "type": "boolean" },
|
||||||
|
"sprint_partition": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/partition" }
|
||||||
|
},
|
||||||
|
"unknown_sprint": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"missing_artifacts": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"self_module_excluded": { "type": "string", "const": "qa/acceptance" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roadmap": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["sprint_ids", "metric_ids"],
|
||||||
|
"properties": {
|
||||||
|
"sprint_ids": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"metric_ids": { "type": "array", "items": { "type": "string" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"row": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"kind",
|
||||||
|
"sprint",
|
||||||
|
"roadmap_line",
|
||||||
|
"statement",
|
||||||
|
"status",
|
||||||
|
"modules",
|
||||||
|
"artifacts",
|
||||||
|
"out_of_scope",
|
||||||
|
"n_modules",
|
||||||
|
"all_modules_gated",
|
||||||
|
"all_artifacts_exist",
|
||||||
|
"has_evidence"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"kind": { "type": "string", "enum": ["sprint_deliverable", "mvp_metric"] },
|
||||||
|
"sprint": { "type": ["string", "null"] },
|
||||||
|
"roadmap_line": { "type": "integer", "minimum": 1 },
|
||||||
|
"statement": { "type": "string", "minLength": 1 },
|
||||||
|
"status": { "type": "string", "enum": ["in_repo", "out_of_scope"] },
|
||||||
|
"modules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/evmodule" }
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/evartifact" }
|
||||||
|
},
|
||||||
|
"out_of_scope": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/oos" }
|
||||||
|
},
|
||||||
|
"n_modules": { "type": "integer", "minimum": 0 },
|
||||||
|
"all_modules_gated": { "type": "boolean" },
|
||||||
|
"all_artifacts_exist": { "type": "boolean" },
|
||||||
|
"has_evidence": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evmodule": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["module", "ci_job", "gated"],
|
||||||
|
"properties": {
|
||||||
|
"module": { "type": "string", "minLength": 1 },
|
||||||
|
"ci_job": { "type": ["string", "null"] },
|
||||||
|
"gated": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evartifact": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["path", "exists"],
|
||||||
|
"properties": {
|
||||||
|
"path": { "type": "string", "minLength": 1 },
|
||||||
|
"exists": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oos": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["item", "source"],
|
||||||
|
"properties": {
|
||||||
|
"item": { "type": "string", "minLength": 1 },
|
||||||
|
"source": { "type": "string", "minLength": 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"partition": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["sprint", "expected", "cited", "missing", "extra", "exact"],
|
||||||
|
"properties": {
|
||||||
|
"sprint": { "type": "string", "minLength": 1 },
|
||||||
|
"expected": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"cited": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"missing": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"extra": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"exact": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Matrice d'acceptation / traçabilité MVP (Sprint 8 · QA · buffer L75).
|
||||||
|
|
||||||
|
Document de RECETTE de MÉTA-NIVEAU : il mappe chaque PROMESSE de la roadmap
|
||||||
|
(8 livrables de sprint L33..L76 + 7 métriques succès MVP L81..L87) vers sa PREUVE
|
||||||
|
livrée (un module gated du CI) OU un hors-périmètre worker SOURCÉ. Il répond à la
|
||||||
|
seule question de recette : « le MVP promis est-il honoré, et ce qui reste
|
||||||
|
est-il explicitement hors périmètre worker ? »
|
||||||
|
|
||||||
|
Axe distinct des autres harnais méta (non redondant · #5) :
|
||||||
|
- qa/audit_4big note la QUALITÉ statique de chaque module ;
|
||||||
|
- qa/regression prouve que chaque suite S'EXÉCUTE au vert ;
|
||||||
|
- devops/deploy_runbook ORDONNE le déploiement VPS des modules ;
|
||||||
|
- qa/acceptance (ici) prouve la COUVERTURE des promesses roadmap.
|
||||||
|
|
||||||
|
Périmètre PROUVÉ, pas déclaré : l'ensemble des modules-preuve est confronté à
|
||||||
|
`.gitea/workflows/ci.yml` (réutilise `q4lib.registry.parse_ci` — zéro duplication
|
||||||
|
· #5) de façon BIJECTIVE, et la fenêtre de sprint de chaque module est LUE dans
|
||||||
|
le registre de l'auditeur 4Big (anti-dérive). Tout module gated non tracé OU tout
|
||||||
|
module tracé non gated → génération refusée.
|
||||||
|
|
||||||
|
Anti-invention (#6) : les nombres des énoncés (<1h, 95/100, 7 dashboards, 2
|
||||||
|
stores) sont des CITATIONS verbatim de la roadmap ; la matrice ne PRÉTEND aucune
|
||||||
|
mesure de performance ; tout hors-périmètre porte une source.
|
||||||
|
|
||||||
|
Produit :
|
||||||
|
- acceptance_matrix.json → la matrice (une ligne par promesse : preuves
|
||||||
|
résolues, hors-périmètre sourcé, drapeaux) + verdict.
|
||||||
|
- MANIFEST.json → manifeste agrégé (comptes, preuve de couverture
|
||||||
|
bijective vs CI, partition par sprint).
|
||||||
|
|
||||||
|
Sous-commandes :
|
||||||
|
build [-o OUT] → écrit acceptance_matrix.json / MANIFEST.json (après validation)
|
||||||
|
validate [-o OUT] → (re)génère en mémoire, valide vs acceptance.schema.json +
|
||||||
|
invariants — sort en erreur si un invariant casse.
|
||||||
|
|
||||||
|
Sortie déterministe (ordre du spec, listes triées, aucun horodatage).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
from acclib import builder, deps # noqa: E402
|
||||||
|
|
||||||
|
_SPEC_PATH = os.path.join(_HERE, "acceptance_spec.json")
|
||||||
|
_SCHEMA_PATH = os.path.join(_HERE, "acceptance.schema.json")
|
||||||
|
_DEFAULT_OUT = os.path.join(_HERE, "out")
|
||||||
|
|
||||||
|
_STATUS = {"in_repo", "out_of_scope"}
|
||||||
|
_KINDS = {"sprint_deliverable", "mvp_metric"}
|
||||||
|
|
||||||
|
|
||||||
|
def _eprint(*args) -> None:
|
||||||
|
print(*args, file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _load(path: str) -> dict:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: str, data) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(data, fh, ensure_ascii=False, indent=2)
|
||||||
|
fh.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bundle() -> dict:
|
||||||
|
"""Charge le spec, lit la réalité du CI + le registre 4Big, assemble."""
|
||||||
|
spec = _load(_SPEC_PATH)
|
||||||
|
ci = deps.parse_ci()
|
||||||
|
audit = deps.audit_sprints()
|
||||||
|
return builder.build_matrix(spec, ci, audit, deps)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bundle(bundle: dict, spec: dict, ci: dict, audit: dict) -> list[str]:
|
||||||
|
"""Valide la matrice contre acceptance.schema.json + invariants de recette.
|
||||||
|
|
||||||
|
Fonction pure sur (bundle, spec, ci, audit) → les tests injectent des
|
||||||
|
dictionnaires synthétiques pour prouver le basculement en erreur.
|
||||||
|
"""
|
||||||
|
schema = _load(_SCHEMA_PATH)
|
||||||
|
errors = list(deps.validate(bundle, schema))
|
||||||
|
|
||||||
|
matrix = bundle["matrix"]
|
||||||
|
manifest = bundle["manifest"]
|
||||||
|
cov = manifest["coverage"]
|
||||||
|
self_module = spec["self_module"]
|
||||||
|
|
||||||
|
deliverables = builder.all_deliverables(spec)
|
||||||
|
|
||||||
|
# INV1 · ids uniques ; livrables de sprint = S1..S8 ordonnés ; métriques =
|
||||||
|
# M1..M7 ordonnées (aucune promesse roadmap omise ni dupliquée).
|
||||||
|
ids = [r["id"] for r in matrix]
|
||||||
|
if len(set(ids)) != len(ids):
|
||||||
|
errors.append(f"lignes: ids en double {ids}.")
|
||||||
|
sprint_ids = [d["id"] for d in spec.get("sprint_deliverables", [])]
|
||||||
|
metric_ids = [d["id"] for d in spec.get("mvp_metrics", [])]
|
||||||
|
if sprint_ids != sorted(sprint_ids):
|
||||||
|
errors.append(f"livrables de sprint non ordonnés {sprint_ids}.")
|
||||||
|
if metric_ids != sorted(metric_ids):
|
||||||
|
errors.append(f"métriques MVP non ordonnées {metric_ids}.")
|
||||||
|
|
||||||
|
# INV2 · toute preuve module citée est gated (aucune preuve fantôme). On
|
||||||
|
# RECALCULE l'ensemble gated depuis le CI (on ne fait pas confiance au
|
||||||
|
# manifeste).
|
||||||
|
gated = set(builder.gated_modules(ci, self_module))
|
||||||
|
for r in matrix:
|
||||||
|
for m in r["modules"]:
|
||||||
|
if m["module"] not in gated:
|
||||||
|
errors.append(f"ligne {r['id']}: preuve non gated {m['module']!r}.")
|
||||||
|
if m["gated"] != (m["module"] in gated):
|
||||||
|
errors.append(f"ligne {r['id']}: drapeau `gated` faux pour {m['module']!r}.")
|
||||||
|
|
||||||
|
# INV3 · COUVERTURE BIJECTIVE (cœur anti-invention · #6) : les modules cités
|
||||||
|
# (lignes in_repo) == l'ensemble gated (hors self). Aucun oubli, aucune
|
||||||
|
# invention.
|
||||||
|
cited: set[str] = set()
|
||||||
|
for d in deliverables:
|
||||||
|
if d.get("status") == "in_repo":
|
||||||
|
cited.update(d.get("evidence_modules", []))
|
||||||
|
missing = sorted(gated - cited)
|
||||||
|
phantom = sorted(cited - gated)
|
||||||
|
if missing:
|
||||||
|
errors.append(f"couverture: modules gated non tracés {missing}.")
|
||||||
|
if phantom:
|
||||||
|
errors.append(f"couverture: preuves non gated {phantom}.")
|
||||||
|
if not cov["bijective"] and not (missing or phantom):
|
||||||
|
errors.append("couverture: `bijective=false` mais aucun écart recalculé.")
|
||||||
|
if cov["bijective"] and (missing or phantom):
|
||||||
|
errors.append("couverture: `bijective=true` mais écart recalculé présent.")
|
||||||
|
|
||||||
|
# INV4 · la fenêtre de sprint de chaque module gated est connue (registre
|
||||||
|
# 4Big + extra du spec) — aucun module gated sans sprint.
|
||||||
|
ms_map = builder.module_sprint_map(spec, audit)
|
||||||
|
unknown = sorted(m for m in gated if m not in ms_map)
|
||||||
|
if unknown:
|
||||||
|
errors.append(f"sprint: modules gated sans fenêtre {unknown}.")
|
||||||
|
for path, meta in spec.get("extra_module_sprint", {}).items():
|
||||||
|
if path in audit and audit[path] != meta["sprint"]:
|
||||||
|
errors.append(
|
||||||
|
f"sprint: extra {path!r} en conflit avec le registre 4Big "
|
||||||
|
f"({meta['sprint']} vs {audit[path]})."
|
||||||
|
)
|
||||||
|
|
||||||
|
# INV5 · PARTITION EXACTE par sprint : chaque livrable de sprint SX cite
|
||||||
|
# EXACTEMENT les modules gated de fenêtre SX (ni trou ni chevauchement).
|
||||||
|
gated_by_sprint: dict[str, set] = {}
|
||||||
|
for m in gated:
|
||||||
|
if m in ms_map:
|
||||||
|
gated_by_sprint.setdefault(ms_map[m], set()).add(m)
|
||||||
|
for d in spec.get("sprint_deliverables", []):
|
||||||
|
sx = d["sprint"]
|
||||||
|
want = gated_by_sprint.get(sx, set())
|
||||||
|
have = set(d.get("evidence_modules", []))
|
||||||
|
if want != have:
|
||||||
|
errors.append(
|
||||||
|
f"partition {sx}: attendu {sorted(want)} ≠ cité {sorted(have)}."
|
||||||
|
)
|
||||||
|
if cov["partition_ok"] != all(p["exact"] for p in cov["sprint_partition"]) or \
|
||||||
|
(cov["unknown_sprint"] and cov["partition_ok"]):
|
||||||
|
errors.append("partition: drapeau `partition_ok` incohérent.")
|
||||||
|
|
||||||
|
# INV6 · tout hors-périmètre porte une source non vide (aucune omission
|
||||||
|
# silencieuse d'une promesse roadmap).
|
||||||
|
for d in deliverables:
|
||||||
|
for o in d.get("out_of_scope", []):
|
||||||
|
if not o.get("source", "").strip():
|
||||||
|
errors.append(f"ligne {d['id']}: hors-périmètre sans source: {o.get('item')!r}.")
|
||||||
|
|
||||||
|
# INV7 · cohérence statut/preuve : in_repo ⇒ ≥1 module OU ≥1 artefact ;
|
||||||
|
# out_of_scope ⇒ 0 module + ≥1 hors-périmètre sourcé. Tout artefact cité
|
||||||
|
# existe sur disque (fait · #6).
|
||||||
|
for d in deliverables:
|
||||||
|
st = d.get("status")
|
||||||
|
if st not in _STATUS:
|
||||||
|
errors.append(f"ligne {d['id']}: statut invalide {st!r}.")
|
||||||
|
mods = d.get("evidence_modules", [])
|
||||||
|
arts = d.get("evidence_artifacts", [])
|
||||||
|
oos = d.get("out_of_scope", [])
|
||||||
|
if st == "in_repo" and not mods and not arts:
|
||||||
|
errors.append(f"ligne {d['id']}: in_repo sans aucune preuve.")
|
||||||
|
if st == "out_of_scope":
|
||||||
|
if mods:
|
||||||
|
errors.append(f"ligne {d['id']}: out_of_scope avec preuve module {mods}.")
|
||||||
|
if not oos:
|
||||||
|
errors.append(f"ligne {d['id']}: out_of_scope sans hors-périmètre sourcé.")
|
||||||
|
for a in arts:
|
||||||
|
if not deps.artifact_exists(a):
|
||||||
|
errors.append(f"ligne {d['id']}: artefact-preuve inexistant {a!r}.")
|
||||||
|
|
||||||
|
# INV8 · kind cohérent : sprint_deliverable ⇒ sprint renseigné ; mvp_metric
|
||||||
|
# ⇒ sprint null (les métriques transcendent les sprints).
|
||||||
|
for d in deliverables:
|
||||||
|
if d["kind"] not in _KINDS:
|
||||||
|
errors.append(f"ligne {d['id']}: kind invalide {d['kind']!r}.")
|
||||||
|
if d["kind"] == "sprint_deliverable" and not d.get("sprint"):
|
||||||
|
errors.append(f"ligne {d['id']}: sprint_deliverable sans sprint.")
|
||||||
|
if d["kind"] == "mvp_metric" and d.get("sprint") is not None:
|
||||||
|
errors.append(f"ligne {d['id']}: mvp_metric avec sprint non nul {d.get('sprint')!r}.")
|
||||||
|
|
||||||
|
# INV9 · le manifeste s'exclut lui-même (SoD · ISA 315) — la matrice ne se
|
||||||
|
# cite jamais comme preuve.
|
||||||
|
if self_module in cited:
|
||||||
|
errors.append("SoD: la matrice se cite elle-même comme preuve.")
|
||||||
|
if cov["self_module_excluded"] != self_module:
|
||||||
|
errors.append("SoD: self_module_excluded divergent.")
|
||||||
|
|
||||||
|
# INV10 · roadmap_line = entier positif ; pas de doublon de ligne.
|
||||||
|
lines = [d["roadmap_line"] for d in deliverables]
|
||||||
|
if any(not isinstance(x, int) or x < 1 for x in lines):
|
||||||
|
errors.append(f"roadmap_line: valeur non positive {lines}.")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_or_die(bundle: dict) -> None:
|
||||||
|
spec = _load(_SPEC_PATH)
|
||||||
|
ci = deps.parse_ci()
|
||||||
|
audit = deps.audit_sprints()
|
||||||
|
errs = validate_bundle(bundle, spec, ci, audit)
|
||||||
|
if errs:
|
||||||
|
_eprint("❌ Validation échouée :")
|
||||||
|
for e in errs:
|
||||||
|
_eprint(f" · {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_build(args) -> None:
|
||||||
|
bundle = _build_bundle()
|
||||||
|
_validate_or_die(bundle)
|
||||||
|
out_dir = args.out or _DEFAULT_OUT
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
_write_json(os.path.join(out_dir, "acceptance_matrix.json"),
|
||||||
|
{"matrix": bundle["matrix"], "verdict": bundle["verdict"]})
|
||||||
|
_write_json(os.path.join(out_dir, "MANIFEST.json"), bundle["manifest"])
|
||||||
|
cov = bundle["manifest"]["coverage"]
|
||||||
|
c = bundle["manifest"]["counts"]
|
||||||
|
verdict = "✅ RECETTE PASS" if bundle["verdict"] else "❌ RECETTE FAIL"
|
||||||
|
print(f"{verdict} — {c['sprint_deliverables']} livrables de sprint + "
|
||||||
|
f"{c['mvp_metrics']} métriques MVP · {c['gated_modules']} modules gated "
|
||||||
|
f"tracés (bijectif={cov['bijective']}) · {c['out_of_scope_items']} "
|
||||||
|
f"hors-périmètre sourcés.")
|
||||||
|
print(f" → {os.path.join(out_dir, 'acceptance_matrix.json')}")
|
||||||
|
print(f" → {os.path.join(out_dir, 'MANIFEST.json')}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_validate(args) -> None:
|
||||||
|
bundle = _build_bundle()
|
||||||
|
_validate_or_die(bundle)
|
||||||
|
cov = bundle["manifest"]["coverage"]
|
||||||
|
print(f"✅ Matrice valide — bijective={cov['bijective']} · "
|
||||||
|
f"partition_ok={cov['partition_ok']} · verdict={bundle['verdict']}.")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> None:
|
||||||
|
p = argparse.ArgumentParser(
|
||||||
|
prog="acceptance_gen",
|
||||||
|
description="Matrice d'acceptation / traçabilité MVP (roadmap → preuve).")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
b = sub.add_parser("build", help="écrit acceptance_matrix.json / MANIFEST.json")
|
||||||
|
b.add_argument("-o", "--out", help="répertoire de sortie (défaut: ./out)")
|
||||||
|
b.set_defaults(func=cmd_build)
|
||||||
|
|
||||||
|
v = sub.add_parser("validate", help="valide sans écrire")
|
||||||
|
v.add_argument("-o", "--out", help="(ignoré · symétrie CLI)")
|
||||||
|
v.set_defaults(func=cmd_validate)
|
||||||
|
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
{
|
||||||
|
"matrix": "MVP Acceptance / Traçabilité roadmap → livrable → preuve",
|
||||||
|
"version": "1.0",
|
||||||
|
"reference_cadre": "Roadmap 04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · livrables de sprint (L33..L76) + métriques succès MVP (L81..L87) · CLAUDE.md #8 (VPS pour TOUS projets · le worker n'écrit jamais sur le VPS) · Sprint 8 « Bufer pour corrections finales » (L75)",
|
||||||
|
"roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md",
|
||||||
|
"self_module": "qa/acceptance",
|
||||||
|
"cible_portage": "Michel / comité de recette — document de recette (hand-off démo publique)",
|
||||||
|
"extra_module_sprint": {
|
||||||
|
"qa/audit_4big": {
|
||||||
|
"sprint": "S7",
|
||||||
|
"source": "roadmap Sprint 7 · QA « Audit 4Big niveau 95+/100 sur 100% deliverables » (L69) — l'auditeur ne figure pas dans son propre registre (SoD · ISA 315), sa fenêtre de sprint est déclarée ici avec source"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sprint_deliverables": [
|
||||||
|
{
|
||||||
|
"id": "S1",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S1",
|
||||||
|
"roadmap_line": 33,
|
||||||
|
"statement": "Repo Gitea live · gap analysis complète · baseline tests OK",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": [],
|
||||||
|
"evidence_artifacts": ["GAP_ANALYSIS_SPRINT1.md"],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "baseline Playwright /crm · /waf-home · /qa (dépend du site LIVE) et push initial VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS pour TOUS projets · worker n'écrit jamais sur le VPS)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S2",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S2",
|
||||||
|
"roadmap_line": 39,
|
||||||
|
"statement": "7 dashboards uniformes · CRM avec pipeline live · RBAC 50 rôles",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": [
|
||||||
|
"publiciste",
|
||||||
|
"rbac",
|
||||||
|
"rbac/fixtures_gen",
|
||||||
|
"rbac/userperm_gen",
|
||||||
|
"rbac/roleprofile_gen",
|
||||||
|
"rbac/apply_plan",
|
||||||
|
"faisabilite/generator"
|
||||||
|
],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "import réel des fixtures RBAC + rendu HTML des dashboards sur le desk ERPNext LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #1 (ERPNext natif = priorité — import via bench par agent Backend)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S3",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S3",
|
||||||
|
"roadmap_line": 46,
|
||||||
|
"statement": "P07 Aqua Terra faisabilité complète auto-générée en <1h",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["faisabilite/bancable"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "pipeline Blender→IFC→Speckle→Flux (aec.py) exécuté sur runtime BIM VPS + viewer 3D console LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — le rendu 3D/BIM tourne hors repo (bim-cloud.otov7.com)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S4",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S4",
|
||||||
|
"roadmap_line": 52,
|
||||||
|
"statement": "5 portails RBAC live · workflow vente end-to-end",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": [
|
||||||
|
"crm/workflow_vente",
|
||||||
|
"crm/dossier_vente",
|
||||||
|
"crm/commissions",
|
||||||
|
"fiscal/ecf_dgii",
|
||||||
|
"frontend/portails"
|
||||||
|
],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S5",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S5",
|
||||||
|
"roadmap_line": 58,
|
||||||
|
"statement": "Dépôts P05/P07 ONAPI · Apps mobile live 2 stores",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["legal/confotur", "qa/audit_5d"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "dépôt ONAPI/CONFOTUR réel + builds Expo 54 et submit App Store / Play Store",
|
||||||
|
"source": "roadmap Sprint 5 · Mobile (L57) — builds & submissions stores hors repo · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S6",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S6",
|
||||||
|
"roadmap_line": 64,
|
||||||
|
"statement": "Indexation Google trilingue · OTOIA voice orchestre AEC",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["seo", "frontend/chat_otoia"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "indexation Google réelle (crawl runtime) + voix Amélie QC (multilingual_v2) exécutée sur runtime OTOIA VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — indexation et TTS sont des services runtime hors repo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S7",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S7",
|
||||||
|
"roadmap_line": 70,
|
||||||
|
"statement": "Plateforme prête pour démo publique (polish · scénarios démo · audit 4Big 95+)",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["demo/scenarios", "qa/audit_4big"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "polish final du site otov7.com (Hero · nav) sur le site LIVE",
|
||||||
|
"source": "roadmap Sprint 7 · Frontend « Site otov7.com polish final » (L67) — dépend du site LIVE · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S8",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S8",
|
||||||
|
"roadmap_line": 76,
|
||||||
|
"statement": "Plateforme démo live · 5 portails · CRM · BIM auto · OTOIA voice",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["qa/regression", "devops/deploy_runbook"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "déploiement production complet + monitoring exécutés sur le VPS (run-book appliqué)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — l'exécution du run-book de déploiement revient à l'agent DevOps / ERPNext Backend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mvp_metrics": [
|
||||||
|
{
|
||||||
|
"id": "M1",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 81,
|
||||||
|
"statement": "Faisabilité auto générée en <1h (vs 3-5 jours actuels)",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["publiciste", "faisabilite/generator", "faisabilite/bancable"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M2",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 82,
|
||||||
|
"statement": "Score 4Big >=95/100 sur 100% deliverables",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["qa/audit_4big"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M3",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 83,
|
||||||
|
"statement": "7 dashboards entités uniformes luxury",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["frontend/portails", "rbac/roleprofile_gen"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "rendu luxury dark+doré sur le desk ERPNext LIVE (Custom HTML)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #4 (design luxury rendu côté desk)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M4",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 84,
|
||||||
|
"statement": "CRM ERPNext natif fonctionnel end-to-end",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["crm/workflow_vente", "crm/dossier_vente", "crm/commissions"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M5",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 85,
|
||||||
|
"statement": "Mobile app publié 2 stores",
|
||||||
|
"status": "out_of_scope",
|
||||||
|
"evidence_modules": [],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "builds Expo 54 + submit App Store #32 / Play Store — aucune fabrication en repo",
|
||||||
|
"source": "roadmap Sprint 5 · Mobile (L56-57) — builds & submissions hors repo · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M6",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 86,
|
||||||
|
"statement": "SEO trilingue indexé",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["seo"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "indexation Google réelle (soumission sitemaps + crawl)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — worker systemd SEO tourne hors repo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M7",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 87,
|
||||||
|
"statement": "OTOIA voice orchestre bout-en-bout",
|
||||||
|
"status": "in_repo",
|
||||||
|
"evidence_modules": ["frontend/chat_otoia"],
|
||||||
|
"evidence_artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "voix Amélie QC (multilingual_v2) + orchestration AEC exécutées sur runtime OTOIA VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — TTS et pilotage aec.py sont des services runtime hors repo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": [
|
||||||
|
"Anti-invention (#6) : aucun chiffre metier n'est fabrique. Les nombres presents dans les enonces (<1h, 95/100, 7 dashboards, 2 stores) sont des CITATIONS verbatim de la roadmap, pas des mesures inventees ; la matrice ne PRETEND aucune mesure de performance (pas de « genere en 42 min »).",
|
||||||
|
"Perimetre PROUVE : l'ensemble des modules-preuve est confronte de facon BIJECTIVE a la realite du CI (parse_ci) — un module gated non cite OU un module cite non gated => generation refusee.",
|
||||||
|
"Cross-coherence : la fenetre de sprint de chaque module est DERIVEE du registre de l'auditeur 4Big (qa/audit_4big/quality_spec.json · zero duplication · #5), jamais re-declaree ici (sauf l'auditeur lui-meme, avec source)."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""acclib · briques de la Matrice d'acceptation / traçabilité MVP (Sprint 8 · QA).
|
||||||
|
|
||||||
|
Document de MÉTA-NIVEAU orienté RECETTE : il mappe chaque PROMESSE de la roadmap
|
||||||
|
(8 livrables de sprint L33..L76 + 7 métriques succès MVP L81..L87) vers sa PREUVE
|
||||||
|
livrée (un module gated du CI) OU un hors-périmètre SOURCÉ (VPS #8 · builds
|
||||||
|
stores · services runtime). Axe distinct des autres harnais méta :
|
||||||
|
|
||||||
|
- qa/audit_4big → note la QUALITÉ statique de chaque module ;
|
||||||
|
- qa/regression → prouve que chaque suite S'EXÉCUTE au vert ;
|
||||||
|
- devops/deploy_runbook→ ORDONNE le déploiement VPS des modules ;
|
||||||
|
- qa/acceptance (ici) → prouve que chaque PROMESSE roadmap est HONORÉE
|
||||||
|
(couverte par un livrable) ou explicitement hors
|
||||||
|
périmètre worker — la RECETTE du mandat.
|
||||||
|
|
||||||
|
deps → réutilise q4lib.registry.parse_ci + le validateur maison Publiciste
|
||||||
|
+ le registre de sprints de l'auditeur 4Big (zéro duplication · #5)
|
||||||
|
builder → assemble la matrice déterministe + le manifeste (preuve de couverture
|
||||||
|
bijective vs CI + partition par sprint + hors-périmètre sourcé)
|
||||||
|
|
||||||
|
Anti-invention (#6) : les nombres des énoncés sont des citations verbatim de la
|
||||||
|
roadmap ; aucune mesure de performance n'est fabriquée ; tout module-preuve est
|
||||||
|
confronté au CI ; tout hors-périmètre porte une source.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""Assemblage de la matrice d'acceptation à partir du spec + de la réalité CI.
|
||||||
|
|
||||||
|
La matrice mappe chaque promesse roadmap → preuve gated OU hors-périmètre sourcé.
|
||||||
|
La liste des modules-preuve n'est PAS crue sur parole : elle est confrontée à
|
||||||
|
`.gitea/workflows/ci.yml` (via q4lib.registry.parse_ci, réutilisé) de façon
|
||||||
|
BIJECTIVE, et la fenêtre de sprint de chaque module est LUE dans le registre de
|
||||||
|
l'auditeur 4Big (zéro re-déclaration · anti-dérive).
|
||||||
|
|
||||||
|
Fonctions PURES (aucune I/O, aucun horodatage) → sortie déterministe, diffable,
|
||||||
|
re-générable bit-à-bit. Le CLI injecte le spec chargé, `parse_ci()` et
|
||||||
|
`audit_sprints()` ; les tests injectent des dictionnaires synthétiques (facile à
|
||||||
|
mettre en défaut → tests négatifs).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def gated_modules(ci: dict, self_module: str) -> list[str]:
|
||||||
|
"""Chemins de modules gated par le CI, hors la matrice elle-même (SoD)."""
|
||||||
|
paths = set(ci.get("job_to_path", {}).values())
|
||||||
|
paths.discard(self_module)
|
||||||
|
return sorted(paths)
|
||||||
|
|
||||||
|
|
||||||
|
def _path_to_job(ci: dict) -> dict[str, str]:
|
||||||
|
"""Inverse job→path en path→job (job minimal en cas de collision)."""
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for job, path in ci.get("job_to_path", {}).items():
|
||||||
|
if path not in out or job < out[path]:
|
||||||
|
out[path] = job
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def module_sprint_map(spec: dict, audit_sprints: dict) -> dict[str, str]:
|
||||||
|
"""{module -> sprint}, dérivé du registre 4Big + complété par le spec.
|
||||||
|
|
||||||
|
Le registre de l'auditeur est la source ; l'auditeur lui-même n'y figure pas
|
||||||
|
(SoD) → son sprint vient de `extra_module_sprint` (avec source). Une entrée
|
||||||
|
`extra` qui écraserait un module déjà daté par le registre est un conflit :
|
||||||
|
on la garde ici mais l'incohérence est signalée par l'invariant du CLI."""
|
||||||
|
out = dict(audit_sprints)
|
||||||
|
for path, meta in spec.get("extra_module_sprint", {}).items():
|
||||||
|
out.setdefault(path, meta["sprint"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def all_deliverables(spec: dict) -> list[dict]:
|
||||||
|
"""Livrables de sprint + métriques MVP, dans l'ordre du spec."""
|
||||||
|
return list(spec.get("sprint_deliverables", [])) + list(spec.get("mvp_metrics", []))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_row(d: dict, gated_set: set, p2j: dict, deps_mod) -> dict[str, Any]:
|
||||||
|
"""Une ligne de matrice : preuves résolues + drapeaux de cohérence."""
|
||||||
|
ev_modules = list(d.get("evidence_modules", []))
|
||||||
|
modules = [
|
||||||
|
{"module": m, "ci_job": p2j.get(m), "gated": m in gated_set}
|
||||||
|
for m in ev_modules
|
||||||
|
]
|
||||||
|
artifacts = [
|
||||||
|
{"path": a, "exists": deps_mod.artifact_exists(a)}
|
||||||
|
for a in d.get("evidence_artifacts", [])
|
||||||
|
]
|
||||||
|
oos = [
|
||||||
|
{"item": o["item"], "source": o.get("source")}
|
||||||
|
for o in d.get("out_of_scope", [])
|
||||||
|
]
|
||||||
|
has_evidence = bool(ev_modules) or bool(d.get("evidence_artifacts"))
|
||||||
|
return {
|
||||||
|
"id": d["id"],
|
||||||
|
"kind": d["kind"],
|
||||||
|
"sprint": d.get("sprint"),
|
||||||
|
"roadmap_line": d["roadmap_line"],
|
||||||
|
"statement": d["statement"],
|
||||||
|
"status": d["status"],
|
||||||
|
"modules": modules,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
"out_of_scope": oos,
|
||||||
|
"n_modules": len(ev_modules),
|
||||||
|
"all_modules_gated": all(m in gated_set for m in ev_modules),
|
||||||
|
"all_artifacts_exist": all(a["exists"] for a in artifacts),
|
||||||
|
"has_evidence": has_evidence,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_matrix(spec: dict, ci: dict, audit_sprints: dict, deps_mod) -> dict[str, Any]:
|
||||||
|
"""Consolide spec + CI + registre 4Big en matrice + manifeste agrégé.
|
||||||
|
|
||||||
|
Ne lève JAMAIS : les incohérences (module manquant / en trop / partition de
|
||||||
|
sprint fausse / artefact absent) sont REMONTÉES dans `manifest.coverage`
|
||||||
|
pour que le CLI/les tests les signalent explicitement. Une recette ne doit
|
||||||
|
pas oublier silencieusement une promesse."""
|
||||||
|
self_module = spec["self_module"]
|
||||||
|
gated = gated_modules(ci, self_module)
|
||||||
|
gated_set = set(gated)
|
||||||
|
p2j = _path_to_job(ci)
|
||||||
|
ms_map = module_sprint_map(spec, audit_sprints)
|
||||||
|
|
||||||
|
deliverables = all_deliverables(spec)
|
||||||
|
rows = [_resolve_row(d, gated_set, p2j, deps_mod) for d in deliverables]
|
||||||
|
|
||||||
|
# --- Couverture BIJECTIVE (cœur anti-invention · #6) ------------------
|
||||||
|
# cited = tout module-preuve des lignes IN_REPO ; doit égaler l'ensemble
|
||||||
|
# gated (hors self). Un module gated non cité = promesse orpheline ; un
|
||||||
|
# module cité non gated = preuve fantôme.
|
||||||
|
cited: set[str] = set()
|
||||||
|
for d in deliverables:
|
||||||
|
if d.get("status") == "in_repo":
|
||||||
|
cited.update(d.get("evidence_modules", []))
|
||||||
|
missing = sorted(gated_set - cited) # gated mais non tracé
|
||||||
|
phantom = sorted(cited - gated_set) # tracé mais non gated
|
||||||
|
bijective = not missing and not phantom
|
||||||
|
|
||||||
|
# --- Partition par sprint --------------------------------------------
|
||||||
|
# Chaque livrable de sprint SX doit citer EXACTEMENT les modules gated dont
|
||||||
|
# la fenêtre est SX (ni plus, ni moins). Prouve que la matrice répartit les
|
||||||
|
# livraisons par sprint sans trou ni chevauchement.
|
||||||
|
gated_by_sprint: dict[str, set] = {}
|
||||||
|
unknown_sprint = sorted(m for m in gated if m not in ms_map)
|
||||||
|
for m in gated:
|
||||||
|
if m in ms_map:
|
||||||
|
gated_by_sprint.setdefault(ms_map[m], set()).add(m)
|
||||||
|
|
||||||
|
sprint_partition: list[dict] = []
|
||||||
|
for d in spec.get("sprint_deliverables", []):
|
||||||
|
sx = d["sprint"]
|
||||||
|
want = gated_by_sprint.get(sx, set())
|
||||||
|
have = set(d.get("evidence_modules", []))
|
||||||
|
sprint_partition.append({
|
||||||
|
"sprint": sx,
|
||||||
|
"expected": sorted(want),
|
||||||
|
"cited": sorted(have),
|
||||||
|
"missing": sorted(want - have),
|
||||||
|
"extra": sorted(have - want),
|
||||||
|
"exact": want == have,
|
||||||
|
})
|
||||||
|
partition_ok = all(p["exact"] for p in sprint_partition) and not unknown_sprint
|
||||||
|
|
||||||
|
# --- Artefacts hors-CI (docs) ----------------------------------------
|
||||||
|
missing_artifacts = sorted(
|
||||||
|
a["path"]
|
||||||
|
for r in rows for a in r["artifacts"] if not a["exists"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Complétude roadmap ----------------------------------------------
|
||||||
|
sprint_ids = [d["id"] for d in spec.get("sprint_deliverables", [])]
|
||||||
|
metric_ids = [d["id"] for d in spec.get("mvp_metrics", [])]
|
||||||
|
oos_count = sum(len(r["out_of_scope"]) for r in rows)
|
||||||
|
in_repo = sum(1 for r in rows if r["status"] == "in_repo")
|
||||||
|
out_of_scope_full = sum(1 for r in rows if r["status"] == "out_of_scope")
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"generated_from": f"acceptance_spec.json v{spec.get('version')}",
|
||||||
|
"reference_cadre": spec.get("reference_cadre"),
|
||||||
|
"roadmap_ref": spec.get("roadmap_ref"),
|
||||||
|
"cible_portage": spec.get("cible_portage"),
|
||||||
|
"self_module": self_module,
|
||||||
|
"counts": {
|
||||||
|
"sprint_deliverables": len(sprint_ids),
|
||||||
|
"mvp_metrics": len(metric_ids),
|
||||||
|
"deliverables_total": len(deliverables),
|
||||||
|
"in_repo": in_repo,
|
||||||
|
"out_of_scope_full": out_of_scope_full,
|
||||||
|
"gated_modules": len(gated),
|
||||||
|
"cited_modules": len(cited),
|
||||||
|
"out_of_scope_items": oos_count,
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"ci_modules_count": len(gated),
|
||||||
|
"cited_modules_count": len(cited),
|
||||||
|
"bijective": bijective,
|
||||||
|
"missing_in_matrix": missing,
|
||||||
|
"phantom_evidence": phantom,
|
||||||
|
"partition_ok": partition_ok,
|
||||||
|
"sprint_partition": sprint_partition,
|
||||||
|
"unknown_sprint": unknown_sprint,
|
||||||
|
"missing_artifacts": missing_artifacts,
|
||||||
|
"self_module_excluded": self_module,
|
||||||
|
},
|
||||||
|
"roadmap": {
|
||||||
|
"sprint_ids": sprint_ids,
|
||||||
|
"metric_ids": metric_ids,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
verdict = bijective and partition_ok and not missing_artifacts
|
||||||
|
|
||||||
|
return {"matrix": rows, "manifest": manifest, "verdict": verdict}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Réutilisation des briques déjà livrées (workflow #5 · zéro duplication).
|
||||||
|
|
||||||
|
La matrice d'acceptation ne redéfinit RIEN qui existe ailleurs :
|
||||||
|
|
||||||
|
- `parse_ci` : parseur de `.gitea/workflows/ci.yml` de l'auditeur 4Big
|
||||||
|
(`qa/audit_4big/q4lib/registry.py`) — la SEULE autorité
|
||||||
|
sur les jobs de test réellement gated. On le réutilise
|
||||||
|
pour DÉRIVER l'ensemble des modules-preuve (jamais une
|
||||||
|
liste à la main) et prouver la couverture bijective.
|
||||||
|
- `validate` : validateur JSON-Schema maison du Publiciste (draft-07,
|
||||||
|
sous-ensemble) — la matrice est validée SANS pip, car le
|
||||||
|
gate CI Gitea Actions tourne sans réseau (CLAUDE.md #2).
|
||||||
|
- `audit_sprints` : la fenêtre de sprint de chaque module est LUE dans le
|
||||||
|
registre de l'auditeur 4Big (`quality_spec.json`) — une
|
||||||
|
seule source de vérité pour tout le mandat (anti-dérive).
|
||||||
|
|
||||||
|
Import par manipulation de `sys.path` (idiome des modules voisins :
|
||||||
|
`q4lib/deps.py`, `reglib/deps.py`) plutôt que duplication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# qa/acceptance/acclib → 05_deliverables_mvp
|
||||||
|
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
|
||||||
|
_PUB = os.path.join(_DELIVERABLES, "publiciste")
|
||||||
|
_AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big")
|
||||||
|
|
||||||
|
for _p in (_PUB, _AUDIT):
|
||||||
|
if _p not in sys.path:
|
||||||
|
sys.path.insert(0, _p)
|
||||||
|
|
||||||
|
from lib import validator # type: ignore # noqa: E402
|
||||||
|
from q4lib import registry # type: ignore # noqa: E402
|
||||||
|
|
||||||
|
validate = validator.validate
|
||||||
|
parse_ci = registry.parse_ci
|
||||||
|
|
||||||
|
# Racine des livrables, exposée aux autres modules acclib.
|
||||||
|
DELIVERABLES_ROOT = _DELIVERABLES
|
||||||
|
|
||||||
|
_AUDIT_SPEC = os.path.join(_AUDIT, "quality_spec.json")
|
||||||
|
|
||||||
|
|
||||||
|
def audit_sprints() -> dict:
|
||||||
|
"""{chemin module -> sprint} tel que déclaré par le registre de l'auditeur
|
||||||
|
4Big. Une seule source de vérité (anti-dérive · #5) ; l'auditeur ne se
|
||||||
|
référence pas lui-même — son sprint est complété par le spec (extra)."""
|
||||||
|
with open(_AUDIT_SPEC, encoding="utf-8") as fh:
|
||||||
|
spec = json.load(fh)
|
||||||
|
return {m["path"]: m["sprint"] for m in spec["modules"]}
|
||||||
|
|
||||||
|
|
||||||
|
def artifact_exists(rel_path: str) -> bool:
|
||||||
|
"""Vrai ssi l'artefact cité (chemin relatif à 05_deliverables_mvp) existe.
|
||||||
|
|
||||||
|
Fait de disque pur — un artefact-preuve inexistant est un mensonge de
|
||||||
|
couverture (#6)."""
|
||||||
|
return os.path.exists(os.path.join(DELIVERABLES_ROOT, rel_path))
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"validate",
|
||||||
|
"parse_ci",
|
||||||
|
"audit_sprints",
|
||||||
|
"artifact_exists",
|
||||||
|
"DELIVERABLES_ROOT",
|
||||||
|
]
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"generated_from": "acceptance_spec.json v1.0",
|
||||||
|
"reference_cadre": "Roadmap 04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · livrables de sprint (L33..L76) + métriques succès MVP (L81..L87) · CLAUDE.md #8 (VPS pour TOUS projets · le worker n'écrit jamais sur le VPS) · Sprint 8 « Bufer pour corrections finales » (L75)",
|
||||||
|
"roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md",
|
||||||
|
"cible_portage": "Michel / comité de recette — document de recette (hand-off démo publique)",
|
||||||
|
"self_module": "qa/acceptance",
|
||||||
|
"counts": {
|
||||||
|
"sprint_deliverables": 8,
|
||||||
|
"mvp_metrics": 7,
|
||||||
|
"deliverables_total": 15,
|
||||||
|
"in_repo": 14,
|
||||||
|
"out_of_scope_full": 1,
|
||||||
|
"gated_modules": 21,
|
||||||
|
"cited_modules": 21,
|
||||||
|
"out_of_scope_items": 12
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"ci_modules_count": 21,
|
||||||
|
"cited_modules_count": 21,
|
||||||
|
"bijective": true,
|
||||||
|
"missing_in_matrix": [],
|
||||||
|
"phantom_evidence": [],
|
||||||
|
"partition_ok": true,
|
||||||
|
"sprint_partition": [
|
||||||
|
{
|
||||||
|
"sprint": "S1",
|
||||||
|
"expected": [],
|
||||||
|
"cited": [],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S2",
|
||||||
|
"expected": [
|
||||||
|
"faisabilite/generator",
|
||||||
|
"publiciste",
|
||||||
|
"rbac",
|
||||||
|
"rbac/apply_plan",
|
||||||
|
"rbac/fixtures_gen",
|
||||||
|
"rbac/roleprofile_gen",
|
||||||
|
"rbac/userperm_gen"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"faisabilite/generator",
|
||||||
|
"publiciste",
|
||||||
|
"rbac",
|
||||||
|
"rbac/apply_plan",
|
||||||
|
"rbac/fixtures_gen",
|
||||||
|
"rbac/roleprofile_gen",
|
||||||
|
"rbac/userperm_gen"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S3",
|
||||||
|
"expected": [
|
||||||
|
"faisabilite/bancable"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"faisabilite/bancable"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S4",
|
||||||
|
"expected": [
|
||||||
|
"crm/commissions",
|
||||||
|
"crm/dossier_vente",
|
||||||
|
"crm/workflow_vente",
|
||||||
|
"fiscal/ecf_dgii",
|
||||||
|
"frontend/portails"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"crm/commissions",
|
||||||
|
"crm/dossier_vente",
|
||||||
|
"crm/workflow_vente",
|
||||||
|
"fiscal/ecf_dgii",
|
||||||
|
"frontend/portails"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S5",
|
||||||
|
"expected": [
|
||||||
|
"legal/confotur",
|
||||||
|
"qa/audit_5d"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"legal/confotur",
|
||||||
|
"qa/audit_5d"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S6",
|
||||||
|
"expected": [
|
||||||
|
"frontend/chat_otoia",
|
||||||
|
"seo"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"frontend/chat_otoia",
|
||||||
|
"seo"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S7",
|
||||||
|
"expected": [
|
||||||
|
"demo/scenarios",
|
||||||
|
"qa/audit_4big"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"demo/scenarios",
|
||||||
|
"qa/audit_4big"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sprint": "S8",
|
||||||
|
"expected": [
|
||||||
|
"devops/deploy_runbook",
|
||||||
|
"qa/regression"
|
||||||
|
],
|
||||||
|
"cited": [
|
||||||
|
"devops/deploy_runbook",
|
||||||
|
"qa/regression"
|
||||||
|
],
|
||||||
|
"missing": [],
|
||||||
|
"extra": [],
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unknown_sprint": [],
|
||||||
|
"missing_artifacts": [],
|
||||||
|
"self_module_excluded": "qa/acceptance"
|
||||||
|
},
|
||||||
|
"roadmap": {
|
||||||
|
"sprint_ids": [
|
||||||
|
"S1",
|
||||||
|
"S2",
|
||||||
|
"S3",
|
||||||
|
"S4",
|
||||||
|
"S5",
|
||||||
|
"S6",
|
||||||
|
"S7",
|
||||||
|
"S8"
|
||||||
|
],
|
||||||
|
"metric_ids": [
|
||||||
|
"M1",
|
||||||
|
"M2",
|
||||||
|
"M3",
|
||||||
|
"M4",
|
||||||
|
"M5",
|
||||||
|
"M6",
|
||||||
|
"M7"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
{
|
||||||
|
"matrix": [
|
||||||
|
{
|
||||||
|
"id": "S1",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S1",
|
||||||
|
"roadmap_line": 33,
|
||||||
|
"statement": "Repo Gitea live · gap analysis complète · baseline tests OK",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"path": "GAP_ANALYSIS_SPRINT1.md",
|
||||||
|
"exists": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "baseline Playwright /crm · /waf-home · /qa (dépend du site LIVE) et push initial VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS pour TOUS projets · worker n'écrit jamais sur le VPS)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 0,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S2",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S2",
|
||||||
|
"roadmap_line": 39,
|
||||||
|
"statement": "7 dashboards uniformes · CRM avec pipeline live · RBAC 50 rôles",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "publiciste",
|
||||||
|
"ci_job": "publiciste-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac",
|
||||||
|
"ci_job": "rbac-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac/fixtures_gen",
|
||||||
|
"ci_job": "rbac-fixtures-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac/userperm_gen",
|
||||||
|
"ci_job": "rbac-userperm-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac/roleprofile_gen",
|
||||||
|
"ci_job": "rbac-roleprofile-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac/apply_plan",
|
||||||
|
"ci_job": "rbac-applyplan-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "faisabilite/generator",
|
||||||
|
"ci_job": "faisabilite-gen-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "import réel des fixtures RBAC + rendu HTML des dashboards sur le desk ERPNext LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #1 (ERPNext natif = priorité — import via bench par agent Backend)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 7,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S3",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S3",
|
||||||
|
"roadmap_line": 46,
|
||||||
|
"statement": "P07 Aqua Terra faisabilité complète auto-générée en <1h",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "faisabilite/bancable",
|
||||||
|
"ci_job": "bancable-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "pipeline Blender→IFC→Speckle→Flux (aec.py) exécuté sur runtime BIM VPS + viewer 3D console LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — le rendu 3D/BIM tourne hors repo (bim-cloud.otov7.com)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 1,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S4",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S4",
|
||||||
|
"roadmap_line": 52,
|
||||||
|
"statement": "5 portails RBAC live · workflow vente end-to-end",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "crm/workflow_vente",
|
||||||
|
"ci_job": "crm-workflow-vente-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "crm/dossier_vente",
|
||||||
|
"ci_job": "crm-dossier-vente-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "crm/commissions",
|
||||||
|
"ci_job": "crm-commissions-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "fiscal/ecf_dgii",
|
||||||
|
"ci_job": "fiscal-ecf-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "frontend/portails",
|
||||||
|
"ci_job": "frontend-portails-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 5,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S5",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S5",
|
||||||
|
"roadmap_line": 58,
|
||||||
|
"statement": "Dépôts P05/P07 ONAPI · Apps mobile live 2 stores",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "legal/confotur",
|
||||||
|
"ci_job": "legal-confotur-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "qa/audit_5d",
|
||||||
|
"ci_job": "qa-audit-5d-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "dépôt ONAPI/CONFOTUR réel + builds Expo 54 et submit App Store / Play Store",
|
||||||
|
"source": "roadmap Sprint 5 · Mobile (L57) — builds & submissions stores hors repo · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 2,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S6",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S6",
|
||||||
|
"roadmap_line": 64,
|
||||||
|
"statement": "Indexation Google trilingue · OTOIA voice orchestre AEC",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "seo",
|
||||||
|
"ci_job": "seo-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "frontend/chat_otoia",
|
||||||
|
"ci_job": "chat-otoia-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "indexation Google réelle (crawl runtime) + voix Amélie QC (multilingual_v2) exécutée sur runtime OTOIA VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — indexation et TTS sont des services runtime hors repo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 2,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S7",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S7",
|
||||||
|
"roadmap_line": 70,
|
||||||
|
"statement": "Plateforme prête pour démo publique (polish · scénarios démo · audit 4Big 95+)",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "demo/scenarios",
|
||||||
|
"ci_job": "demo-scenario-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "qa/audit_4big",
|
||||||
|
"ci_job": "qa-audit-4big-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "polish final du site otov7.com (Hero · nav) sur le site LIVE",
|
||||||
|
"source": "roadmap Sprint 7 · Frontend « Site otov7.com polish final » (L67) — dépend du site LIVE · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 2,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S8",
|
||||||
|
"kind": "sprint_deliverable",
|
||||||
|
"sprint": "S8",
|
||||||
|
"roadmap_line": 76,
|
||||||
|
"statement": "Plateforme démo live · 5 portails · CRM · BIM auto · OTOIA voice",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "qa/regression",
|
||||||
|
"ci_job": "qa-regression-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "devops/deploy_runbook",
|
||||||
|
"ci_job": "devops-deploy-runbook-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "déploiement production complet + monitoring exécutés sur le VPS (run-book appliqué)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — l'exécution du run-book de déploiement revient à l'agent DevOps / ERPNext Backend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 2,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M1",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 81,
|
||||||
|
"statement": "Faisabilité auto générée en <1h (vs 3-5 jours actuels)",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "publiciste",
|
||||||
|
"ci_job": "publiciste-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "faisabilite/generator",
|
||||||
|
"ci_job": "faisabilite-gen-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "faisabilite/bancable",
|
||||||
|
"ci_job": "bancable-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [],
|
||||||
|
"n_modules": 3,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M2",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 82,
|
||||||
|
"statement": "Score 4Big >=95/100 sur 100% deliverables",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "qa/audit_4big",
|
||||||
|
"ci_job": "qa-audit-4big-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [],
|
||||||
|
"n_modules": 1,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M3",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 83,
|
||||||
|
"statement": "7 dashboards entités uniformes luxury",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "frontend/portails",
|
||||||
|
"ci_job": "frontend-portails-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "rbac/roleprofile_gen",
|
||||||
|
"ci_job": "rbac-roleprofile-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "rendu luxury dark+doré sur le desk ERPNext LIVE (Custom HTML)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) · #4 (design luxury rendu côté desk)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 2,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M4",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 84,
|
||||||
|
"statement": "CRM ERPNext natif fonctionnel end-to-end",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "crm/workflow_vente",
|
||||||
|
"ci_job": "crm-workflow-vente-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "crm/dossier_vente",
|
||||||
|
"ci_job": "crm-dossier-vente-tests",
|
||||||
|
"gated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "crm/commissions",
|
||||||
|
"ci_job": "crm-commissions-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [],
|
||||||
|
"n_modules": 3,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M5",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 85,
|
||||||
|
"statement": "Mobile app publié 2 stores",
|
||||||
|
"status": "out_of_scope",
|
||||||
|
"modules": [],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "builds Expo 54 + submit App Store #32 / Play Store — aucune fabrication en repo",
|
||||||
|
"source": "roadmap Sprint 5 · Mobile (L56-57) — builds & submissions hors repo · CLAUDE.md #8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 0,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M6",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 86,
|
||||||
|
"statement": "SEO trilingue indexé",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "seo",
|
||||||
|
"ci_job": "seo-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "indexation Google réelle (soumission sitemaps + crawl)",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — worker systemd SEO tourne hors repo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 1,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "M7",
|
||||||
|
"kind": "mvp_metric",
|
||||||
|
"sprint": null,
|
||||||
|
"roadmap_line": 87,
|
||||||
|
"statement": "OTOIA voice orchestre bout-en-bout",
|
||||||
|
"status": "in_repo",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module": "frontend/chat_otoia",
|
||||||
|
"ci_job": "chat-otoia-tests",
|
||||||
|
"gated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [],
|
||||||
|
"out_of_scope": [
|
||||||
|
{
|
||||||
|
"item": "voix Amélie QC (multilingual_v2) + orchestration AEC exécutées sur runtime OTOIA VPS",
|
||||||
|
"source": "CLAUDE.md #8 (VPS) — TTS et pilotage aec.py sont des services runtime hors repo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_modules": 1,
|
||||||
|
"all_modules_gated": true,
|
||||||
|
"all_artifacts_exist": true,
|
||||||
|
"has_evidence": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"verdict": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip).
|
||||||
|
|
||||||
|
Couvre : le build réel (couverture bijective vs CI Gitea + registre 4Big), la
|
||||||
|
conformité au schéma, les 10 familles d'invariants, le déterminisme, et une
|
||||||
|
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole).
|
||||||
|
|
||||||
|
Les négatifs s'appuient sur une fixture SYNTHÉTIQUE minimale (CI + registre +
|
||||||
|
spec factices) pour contrôler exactement l'écart injecté, sans dépendre de la
|
||||||
|
forme du dépôt réel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_MOD = os.path.normpath(os.path.join(_HERE, ".."))
|
||||||
|
sys.path.insert(0, _MOD)
|
||||||
|
|
||||||
|
import acceptance_gen as gen # noqa: E402
|
||||||
|
from acclib import builder, deps # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _spec() -> dict:
|
||||||
|
return gen._load(gen._SPEC_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Fixture synthétique : deux modules gated (a/one=S2, b/two=S3) + la matrice. #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
_SELF = "qa/acceptance"
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_ci(paths, self_module=_SELF) -> dict:
|
||||||
|
j2p = {f"{p.replace('/', '-')}-tests": p for p in paths}
|
||||||
|
j2p["qa-acceptance-tests"] = self_module
|
||||||
|
return {"job_to_path": j2p, "gate_needs": list(j2p)}
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_audit() -> dict:
|
||||||
|
return {"a/one": "S2", "b/two": "S3"}
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_spec() -> dict:
|
||||||
|
return {
|
||||||
|
"version": "test",
|
||||||
|
"reference_cadre": "fixture",
|
||||||
|
"roadmap_ref": "ROADMAP.md",
|
||||||
|
"self_module": _SELF,
|
||||||
|
"cible_portage": "recette",
|
||||||
|
"extra_module_sprint": {},
|
||||||
|
"sprint_deliverables": [
|
||||||
|
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
|
||||||
|
"roadmap_line": 39, "statement": "livrable S2", "status": "in_repo",
|
||||||
|
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||||
|
"out_of_scope": []},
|
||||||
|
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
|
||||||
|
"roadmap_line": 46, "statement": "livrable S3", "status": "in_repo",
|
||||||
|
"evidence_modules": ["b/two"], "evidence_artifacts": [],
|
||||||
|
"out_of_scope": []},
|
||||||
|
],
|
||||||
|
"mvp_metrics": [
|
||||||
|
{"id": "M1", "kind": "mvp_metric", "sprint": None,
|
||||||
|
"roadmap_line": 81, "statement": "métrique M1", "status": "in_repo",
|
||||||
|
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||||
|
"out_of_scope": []},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_env():
|
||||||
|
"""(spec, ci, audit) — la fixture minimale valide."""
|
||||||
|
return _mini_spec(), _mini_ci(["a/one", "b/two"]), _mini_audit()
|
||||||
|
|
||||||
|
|
||||||
|
def _mini_errors(spec, ci=None, audit=None) -> list:
|
||||||
|
ci = ci or _mini_ci(["a/one", "b/two"])
|
||||||
|
audit = audit or _mini_audit()
|
||||||
|
bundle = builder.build_matrix(spec, ci, audit, deps)
|
||||||
|
return gen.validate_bundle(bundle, spec, ci, audit)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class TestMiniClean(unittest.TestCase):
|
||||||
|
"""La fixture synthétique doit valider proprement (base des négatifs)."""
|
||||||
|
|
||||||
|
def test_mini_validates_clean(self):
|
||||||
|
self.assertEqual(_mini_errors(*(_mini_env()[:1]),), [],
|
||||||
|
"la fixture de base doit être verte")
|
||||||
|
|
||||||
|
def test_mini_bijective(self):
|
||||||
|
spec, ci, audit = _mini_env()
|
||||||
|
b = builder.build_matrix(spec, ci, audit, deps)
|
||||||
|
cov = b["manifest"]["coverage"]
|
||||||
|
self.assertTrue(cov["bijective"])
|
||||||
|
self.assertTrue(cov["partition_ok"])
|
||||||
|
self.assertTrue(b["verdict"])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class TestRealBuild(unittest.TestCase):
|
||||||
|
"""Build réel contre le CI, le registre 4Big et le spec du dépôt."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.spec = _spec()
|
||||||
|
self.ci = deps.parse_ci()
|
||||||
|
self.audit = deps.audit_sprints()
|
||||||
|
self.bundle = builder.build_matrix(self.spec, self.ci, self.audit, deps)
|
||||||
|
|
||||||
|
def test_build_validates_clean(self):
|
||||||
|
errs = gen.validate_bundle(self.bundle, self.spec, self.ci, self.audit)
|
||||||
|
self.assertEqual(errs, [], f"invariants rouges: {errs}")
|
||||||
|
|
||||||
|
def test_schema_conformance(self):
|
||||||
|
schema = gen._load(gen._SCHEMA_PATH)
|
||||||
|
self.assertEqual(list(deps.validate(self.bundle, schema)), [])
|
||||||
|
|
||||||
|
def test_coverage_bijective(self):
|
||||||
|
cov = self.bundle["manifest"]["coverage"]
|
||||||
|
self.assertTrue(cov["bijective"])
|
||||||
|
self.assertEqual(cov["missing_in_matrix"], [])
|
||||||
|
self.assertEqual(cov["phantom_evidence"], [])
|
||||||
|
|
||||||
|
def test_partition_exact_all_sprints(self):
|
||||||
|
cov = self.bundle["manifest"]["coverage"]
|
||||||
|
self.assertTrue(cov["partition_ok"])
|
||||||
|
for p in cov["sprint_partition"]:
|
||||||
|
self.assertTrue(p["exact"], f"partition {p['sprint']} non exacte")
|
||||||
|
self.assertEqual(cov["unknown_sprint"], [])
|
||||||
|
|
||||||
|
def test_verdict_pass(self):
|
||||||
|
self.assertTrue(self.bundle["verdict"])
|
||||||
|
|
||||||
|
def test_counts_match_roadmap(self):
|
||||||
|
c = self.bundle["manifest"]["counts"]
|
||||||
|
self.assertEqual(c["sprint_deliverables"], 8) # S1..S8
|
||||||
|
self.assertEqual(c["mvp_metrics"], 7) # M1..M7
|
||||||
|
self.assertEqual(c["gated_modules"], c["cited_modules"])
|
||||||
|
|
||||||
|
def test_every_gated_module_cited_once(self):
|
||||||
|
gated = set(builder.gated_modules(self.ci, self.spec["self_module"]))
|
||||||
|
cited = set()
|
||||||
|
for d in builder.all_deliverables(self.spec):
|
||||||
|
if d["status"] == "in_repo":
|
||||||
|
cited.update(d["evidence_modules"])
|
||||||
|
self.assertEqual(cited, gated)
|
||||||
|
|
||||||
|
def test_self_module_not_cited(self):
|
||||||
|
for d in builder.all_deliverables(self.spec):
|
||||||
|
self.assertNotIn(self.spec["self_module"], d["evidence_modules"])
|
||||||
|
|
||||||
|
def test_out_of_scope_all_sourced(self):
|
||||||
|
for d in builder.all_deliverables(self.spec):
|
||||||
|
for o in d.get("out_of_scope", []):
|
||||||
|
self.assertTrue(o.get("source", "").strip(),
|
||||||
|
f"{d['id']}: hors-périmètre sans source")
|
||||||
|
|
||||||
|
def test_evidence_artifacts_exist(self):
|
||||||
|
for d in builder.all_deliverables(self.spec):
|
||||||
|
for a in d.get("evidence_artifacts", []):
|
||||||
|
self.assertTrue(deps.artifact_exists(a), f"{a} absent")
|
||||||
|
|
||||||
|
def test_determinism(self):
|
||||||
|
b2 = builder.build_matrix(self.spec, self.ci, self.audit, deps)
|
||||||
|
self.assertEqual(self.bundle, b2)
|
||||||
|
|
||||||
|
def test_sprint_windows_from_audit_registry(self):
|
||||||
|
# Toute fenêtre de sprint vient du registre 4Big (sauf l'auditeur, via
|
||||||
|
# extra) — jamais re-déclarée dans le spec d'acceptation.
|
||||||
|
ms = builder.module_sprint_map(self.spec, self.audit)
|
||||||
|
gated = builder.gated_modules(self.ci, self.spec["self_module"])
|
||||||
|
for m in gated:
|
||||||
|
self.assertIn(m, ms, f"{m} sans fenêtre de sprint")
|
||||||
|
|
||||||
|
def test_audit_4big_declared_via_extra(self):
|
||||||
|
self.assertIn("qa/audit_4big", self.spec["extra_module_sprint"])
|
||||||
|
self.assertTrue(
|
||||||
|
self.spec["extra_module_sprint"]["qa/audit_4big"]["source"].strip())
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class TestNegativeInjections(unittest.TestCase):
|
||||||
|
"""Chaque invariant DOIT rougir quand on injecte l'écart correspondant."""
|
||||||
|
|
||||||
|
def test_inv2_phantom_module_flagged(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["evidence_modules"].append("z/ghost")
|
||||||
|
self.assertTrue(any("non gated" in e or "fantôme" in e
|
||||||
|
for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv3_missing_gated_module(self):
|
||||||
|
# b/two gated mais retiré de son livrable → couverture non bijective.
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||||
|
# laisser un artefact pour ne pas déclencher AUSSI INV7
|
||||||
|
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||||
|
errs = _mini_errors(spec)
|
||||||
|
self.assertTrue(any("non tracés" in e for e in errs), errs)
|
||||||
|
|
||||||
|
def test_inv5_partition_wrong_sprint(self):
|
||||||
|
# Placer b/two (fenêtre S3) dans le livrable S2 → partition fausse.
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", "b/two"]
|
||||||
|
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||||
|
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||||
|
self.assertTrue(any("partition" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv4_module_without_sprint(self):
|
||||||
|
# Registre 4Big ne connaît pas b/two → module gated sans fenêtre.
|
||||||
|
spec = _mini_spec()
|
||||||
|
bad_audit = {"a/one": "S2"} # b/two manquant
|
||||||
|
errs = _mini_errors(spec, audit=bad_audit)
|
||||||
|
self.assertTrue(any("sans fenêtre" in e for e in errs), errs)
|
||||||
|
|
||||||
|
def test_inv4_extra_conflicts_with_registry(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["extra_module_sprint"] = {"a/one": {"sprint": "S9", "source": "x"}}
|
||||||
|
self.assertTrue(any("conflit" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv6_out_of_scope_without_source(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["out_of_scope"] = [{"item": "x", "source": " "}]
|
||||||
|
self.assertTrue(any("sans source" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv7_in_repo_without_evidence(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["mvp_metrics"][0]["evidence_modules"] = []
|
||||||
|
self.assertTrue(any("sans aucune preuve" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv7_out_of_scope_with_module(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["mvp_metrics"].append({
|
||||||
|
"id": "M2", "kind": "mvp_metric", "sprint": None, "roadmap_line": 82,
|
||||||
|
"statement": "m2", "status": "out_of_scope",
|
||||||
|
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||||
|
"out_of_scope": [{"item": "x", "source": "s"}]})
|
||||||
|
self.assertTrue(any("out_of_scope avec preuve" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv7_missing_artifact(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["evidence_artifacts"] = ["does/not/exist.md"]
|
||||||
|
self.assertTrue(any("inexistant" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv8_metric_with_sprint(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["mvp_metrics"][0]["sprint"] = "S4"
|
||||||
|
self.assertTrue(any("sprint non nul" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv8_sprint_deliverable_without_sprint(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["sprint"] = None
|
||||||
|
self.assertTrue(any("sans sprint" in e or "sprint" in e
|
||||||
|
for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv9_self_cited(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
# citer la matrice elle-même comme preuve (et la rendre gated)
|
||||||
|
ci = _mini_ci(["a/one", "b/two"])
|
||||||
|
ci["job_to_path"]["extra-job"] = _SELF # reste self → exclu du gated
|
||||||
|
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", _SELF]
|
||||||
|
self.assertTrue(any("elle-même" in e or "non gated" in e
|
||||||
|
for e in _mini_errors(spec, ci=ci)))
|
||||||
|
|
||||||
|
def test_inv1_duplicate_id(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["mvp_metrics"][0]["id"] = "S2" # collision avec un sprint
|
||||||
|
self.assertTrue(any("double" in e for e in _mini_errors(spec)))
|
||||||
|
|
||||||
|
def test_inv3_flags_bijective_false_consistency(self):
|
||||||
|
# Retirer un module ET son artefact → bijective doit passer à False.
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||||
|
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||||
|
b = builder.build_matrix(spec, _mini_ci(["a/one", "b/two"]), _mini_audit(), deps)
|
||||||
|
self.assertFalse(b["manifest"]["coverage"]["bijective"])
|
||||||
|
self.assertFalse(b["verdict"])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class TestCLI(unittest.TestCase):
|
||||||
|
"""Le CLI build/validate produit les artefacts et sort proprement."""
|
||||||
|
|
||||||
|
def test_build_writes_outputs(self):
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
gen.main(["build", "-o", tmp])
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(tmp, "acceptance_matrix.json")))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(tmp, "MANIFEST.json")))
|
||||||
|
|
||||||
|
def test_validate_exits_zero(self):
|
||||||
|
# cmd_validate ne lève pas sur le dépôt réel (sinon SystemExit).
|
||||||
|
gen.main(["validate"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"audit": "OTO QA · Audit 4Big Qualité",
|
"audit": "OTO QA · Audit 4Big Qualité",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"verdict": "PASS",
|
"verdict": "PASS",
|
||||||
"modules_audited": 20,
|
"modules_audited": 21,
|
||||||
"min_score": 100,
|
"min_score": 100,
|
||||||
"pass_score": 95,
|
"pass_score": 95,
|
||||||
"coverage_ok": true,
|
"coverage_ok": true,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"pass_score": 95,
|
"pass_score": 95,
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"ci_modules_count": 20,
|
"ci_modules_count": 21,
|
||||||
"registry_modules_count": 20,
|
"registry_modules_count": 21,
|
||||||
"missing_in_registry": [],
|
"missing_in_registry": [],
|
||||||
"missing_in_ci": [],
|
"missing_in_ci": [],
|
||||||
"not_in_gate": [],
|
"not_in_gate": [],
|
||||||
@@ -802,6 +802,48 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "qa-acceptance",
|
||||||
|
"path": "qa/acceptance",
|
||||||
|
"sprint": "S8",
|
||||||
|
"archetype": "generator",
|
||||||
|
"applicable_weight": 100,
|
||||||
|
"earned_weight": 100,
|
||||||
|
"score": 100,
|
||||||
|
"verdict": "PASS",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"criterion": "DOC",
|
||||||
|
"weight": 20,
|
||||||
|
"passed": true,
|
||||||
|
"evidence": "README.md (3714 octets)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion": "CONTRAT",
|
||||||
|
"weight": 20,
|
||||||
|
"passed": true,
|
||||||
|
"evidence": "acceptance.schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion": "TESTS",
|
||||||
|
"weight": 25,
|
||||||
|
"passed": true,
|
||||||
|
"evidence": "31 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion": "CLI",
|
||||||
|
"weight": 15,
|
||||||
|
"passed": true,
|
||||||
|
"evidence": "acceptance_gen.py (argparse + __main__)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"criterion": "HANDOFF",
|
||||||
|
"weight": 20,
|
||||||
|
"passed": true,
|
||||||
|
"evidence": "MANIFEST + 1 artefact(s) JSON valides"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "qa-regression",
|
"id": "qa-regression",
|
||||||
"path": "qa/regression",
|
"path": "qa/regression",
|
||||||
@@ -846,8 +888,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"totals": {
|
"totals": {
|
||||||
"modules": 20,
|
"modules": 21,
|
||||||
"pass": 20,
|
"pass": 21,
|
||||||
"fail": 0,
|
"fail": 0,
|
||||||
"min_score": 100,
|
"min_score": 100,
|
||||||
"max_score": 100,
|
"max_score": 100,
|
||||||
|
|||||||
@@ -80,7 +80,8 @@
|
|||||||
{ "id": "frontend-chat-otoia", "path": "frontend/chat_otoia", "sprint": "S6", "archetype": "generator", "source": "roadmap Sprint 6 · Chat OTOIA embarqué (Custom Block) · ci.yml chat-otoia-tests" },
|
{ "id": "frontend-chat-otoia", "path": "frontend/chat_otoia", "sprint": "S6", "archetype": "generator", "source": "roadmap Sprint 6 · Chat OTOIA embarqué (Custom Block) · ci.yml chat-otoia-tests" },
|
||||||
{ "id": "demo-scenarios", "path": "demo/scenarios", "sprint": "S7", "archetype": "generator", "source": "roadmap Sprint 7 · Scénarios démo (P07 banquier / P05 client) · ci.yml demo-scenario-tests" },
|
{ "id": "demo-scenarios", "path": "demo/scenarios", "sprint": "S7", "archetype": "generator", "source": "roadmap Sprint 7 · Scénarios démo (P07 banquier / P05 client) · ci.yml demo-scenario-tests" },
|
||||||
{ "id": "qa-regression", "path": "qa/regression", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · Regression tests exhaustifs (matrice méta-niveau) · ci.yml qa-regression-tests" },
|
{ "id": "qa-regression", "path": "qa/regression", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · Regression tests exhaustifs (matrice méta-niveau) · ci.yml qa-regression-tests" },
|
||||||
{ "id": "devops-deploy-runbook", "path": "devops/deploy_runbook", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · DevOps Deployment production complet (run-book VPS unifié) · ci.yml devops-deploy-runbook-tests" }
|
{ "id": "devops-deploy-runbook", "path": "devops/deploy_runbook", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · DevOps Deployment production complet (run-book VPS unifié) · ci.yml devops-deploy-runbook-tests" },
|
||||||
|
{ "id": "qa-acceptance", "path": "qa/acceptance", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · QA buffer (matrice d'acceptation / traçabilité MVP roadmap → preuve) · ci.yml qa-acceptance-tests" }
|
||||||
],
|
],
|
||||||
"notes": [
|
"notes": [
|
||||||
"Audit de MÉTA-NIVEAU : chaque note de module est CALCULÉE depuis des faits du dépôt (présence README/schéma, nombre de méthodes test_*, entrypoint CLI, intégrité out/) — jamais saisie à la main (#6).",
|
"Audit de MÉTA-NIVEAU : chaque note de module est CALCULÉE depuis des faits du dépôt (présence README/schéma, nombre de méthodes test_*, entrypoint CLI, intégrité out/) — jamais saisie à la main (#6).",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"matrix": "OTO QA · Matrice de régression exhaustive",
|
"matrix": "OTO QA · Matrice de régression exhaustive",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"verdict": "PASS",
|
"verdict": "PASS",
|
||||||
"suites": 20,
|
"suites": 21,
|
||||||
"test_methods": 503,
|
"test_methods": 534,
|
||||||
"coverage_ok": true,
|
"coverage_ok": true,
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
"regression_plan.json"
|
"regression_plan.json"
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
],
|
],
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"suites_count": 20,
|
"suites_count": 21,
|
||||||
"gated_in_ci": 21,
|
"gated_in_ci": 22,
|
||||||
"self_module_excluded": "qa/regression",
|
"self_module_excluded": "qa/regression",
|
||||||
"self_module_gated": true,
|
"self_module_gated": true,
|
||||||
"missing_tests_dir": [],
|
"missing_tests_dir": [],
|
||||||
@@ -157,6 +157,17 @@
|
|||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 23
|
"test_methods": 23
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "qa.acceptance",
|
||||||
|
"path": "qa/acceptance",
|
||||||
|
"jobs": [
|
||||||
|
"qa-acceptance-tests"
|
||||||
|
],
|
||||||
|
"in_gate": true,
|
||||||
|
"has_tests_dir": true,
|
||||||
|
"test_files": 1,
|
||||||
|
"test_methods": 31
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "qa.audit_4big",
|
"id": "qa.audit_4big",
|
||||||
"path": "qa/audit_4big",
|
"path": "qa/audit_4big",
|
||||||
@@ -248,9 +259,9 @@
|
|||||||
],
|
],
|
||||||
"under_threshold": [],
|
"under_threshold": [],
|
||||||
"totals": {
|
"totals": {
|
||||||
"suites": 20,
|
"suites": 21,
|
||||||
"test_files": 20,
|
"test_files": 21,
|
||||||
"test_methods": 503,
|
"test_methods": 534,
|
||||||
"min_methods": 10,
|
"min_methods": 10,
|
||||||
"under_threshold": 0
|
"under_threshold": 0
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user