diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ad70582..067858e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -405,10 +405,30 @@ jobs: - name: Tests run-book de déploiement 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: name: Gate qualité (agrégat) 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: - name: Résultat run: echo "✅ Gate qualité 4Big franchi — tous les checks verts." diff --git a/05_activity_log/2026-07-30.md b/05_activity_log/2026-07-30.md index 9754888..ff3b212 100644 --- a/05_activity_log/2026-07-30.md +++ b/05_activity_log/2026-07-30.md @@ -1,5 +1,70 @@ # 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) **Tâche** : **Sprint 8 · DevOps** — Générateur du **Run-book de déploiement VPS diff --git a/05_deliverables_mvp/daily_reports/2026-07-30-session24.md b/05_deliverables_mvp/daily_reports/2026-07-30-session24.md new file mode 100644 index 0000000..b3e58d1 --- /dev/null +++ b/05_deliverables_mvp/daily_reports/2026-07-30-session24.md @@ -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 diff --git a/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json b/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json index b0a83fc..c2403e1 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json +++ b/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json @@ -89,7 +89,8 @@ "demo/scenarios": "contenu", "qa/audit_5d": "verification-qa", "qa/audit_4big": "verification-qa", - "qa/regression": "verification-qa" + "qa/regression": "verification-qa", + "qa/acceptance": "verification-qa" }, "confirmations": { "custom_modules": { diff --git a/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json b/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json index 4f84cd0..8f542e2 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json +++ b/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json @@ -5,14 +5,14 @@ "self_module": "devops/deploy_runbook", "counts": { "phases": 7, - "modules": 20, - "modules_mapped": 20, + "modules": 21, + "modules_mapped": 21, "confirmations": 7, "confirmations_referenced": 7 }, "coverage": { - "ci_modules_count": 20, - "mapped_modules_count": 20, + "ci_modules_count": 21, + "mapped_modules_count": 21, "bijective": true, "missing_in_map": [], "extra_in_map": [], diff --git a/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json b/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json index 836de52..2659bc7 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json +++ b/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json @@ -203,6 +203,10 @@ "contenu" ], "modules": [ + { + "module": "qa/acceptance", + "ci_job": "qa-acceptance-tests" + }, { "module": "qa/audit_4big", "ci_job": "qa-audit-4big-tests" diff --git a/05_deliverables_mvp/qa/acceptance/.gitignore b/05_deliverables_mvp/qa/acceptance/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/05_deliverables_mvp/qa/acceptance/README.md b/05_deliverables_mvp/qa/acceptance/README.md new file mode 100644 index 0000000..da30a9e --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/README.md @@ -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. diff --git a/05_deliverables_mvp/qa/acceptance/acceptance.schema.json b/05_deliverables_mvp/qa/acceptance/acceptance.schema.json new file mode 100644 index 0000000..ff0571f --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acceptance.schema.json @@ -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" } + } + } + } +} diff --git a/05_deliverables_mvp/qa/acceptance/acceptance_gen.py b/05_deliverables_mvp/qa/acceptance/acceptance_gen.py new file mode 100644 index 0000000..7846007 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acceptance_gen.py @@ -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() diff --git a/05_deliverables_mvp/qa/acceptance/acceptance_spec.json b/05_deliverables_mvp/qa/acceptance/acceptance_spec.json new file mode 100644 index 0000000..bc90a48 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acceptance_spec.json @@ -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)." + ] +} diff --git a/05_deliverables_mvp/qa/acceptance/acclib/__init__.py b/05_deliverables_mvp/qa/acceptance/acclib/__init__.py new file mode 100644 index 0000000..00e5fce --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acclib/__init__.py @@ -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. +""" diff --git a/05_deliverables_mvp/qa/acceptance/acclib/builder.py b/05_deliverables_mvp/qa/acceptance/acclib/builder.py new file mode 100644 index 0000000..3b70950 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acclib/builder.py @@ -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} diff --git a/05_deliverables_mvp/qa/acceptance/acclib/deps.py b/05_deliverables_mvp/qa/acceptance/acclib/deps.py new file mode 100644 index 0000000..975aa13 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/acclib/deps.py @@ -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", +] diff --git a/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json b/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json new file mode 100644 index 0000000..d07adce --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json @@ -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" + ] + } +} diff --git a/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json b/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json new file mode 100644 index 0000000..15c84b8 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json @@ -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 +} diff --git a/05_deliverables_mvp/qa/acceptance/tests/test_acceptance.py b/05_deliverables_mvp/qa/acceptance/tests/test_acceptance.py new file mode 100644 index 0000000..c430cd0 --- /dev/null +++ b/05_deliverables_mvp/qa/acceptance/tests/test_acceptance.py @@ -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() diff --git a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json index 83a797d..0bf2d8f 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json @@ -4,7 +4,7 @@ "audit": "OTO QA · Audit 4Big Qualité", "version": "1.0", "verdict": "PASS", - "modules_audited": 20, + "modules_audited": 21, "min_score": 100, "pass_score": 95, "coverage_ok": true, diff --git a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json index a22b810..76fcf43 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json +++ b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json @@ -5,8 +5,8 @@ "pass_score": 95, "coverage": { "ok": true, - "ci_modules_count": 20, - "registry_modules_count": 20, + "ci_modules_count": 21, + "registry_modules_count": 21, "missing_in_registry": [], "missing_in_ci": [], "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", "path": "qa/regression", @@ -846,8 +888,8 @@ } ], "totals": { - "modules": 20, - "pass": 20, + "modules": 21, + "pass": 21, "fail": 0, "min_score": 100, "max_score": 100, diff --git a/05_deliverables_mvp/qa/audit_4big/quality_spec.json b/05_deliverables_mvp/qa/audit_4big/quality_spec.json index fa35a06..e6c9f07 100644 --- a/05_deliverables_mvp/qa/audit_4big/quality_spec.json +++ b/05_deliverables_mvp/qa/audit_4big/quality_spec.json @@ -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": "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": "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": [ "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).", diff --git a/05_deliverables_mvp/qa/regression/out/MANIFEST.json b/05_deliverables_mvp/qa/regression/out/MANIFEST.json index b916ed3..7e4ff72 100644 --- a/05_deliverables_mvp/qa/regression/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/regression/out/MANIFEST.json @@ -4,8 +4,8 @@ "matrix": "OTO QA · Matrice de régression exhaustive", "version": "1.0", "verdict": "PASS", - "suites": 20, - "test_methods": 503, + "suites": 21, + "test_methods": 534, "coverage_ok": true, "artifacts": [ "regression_plan.json" diff --git a/05_deliverables_mvp/qa/regression/out/regression_plan.json b/05_deliverables_mvp/qa/regression/out/regression_plan.json index df1c4fc..d814952 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_plan.json +++ b/05_deliverables_mvp/qa/regression/out/regression_plan.json @@ -17,8 +17,8 @@ ], "coverage": { "ok": true, - "suites_count": 20, - "gated_in_ci": 21, + "suites_count": 21, + "gated_in_ci": 22, "self_module_excluded": "qa/regression", "self_module_gated": true, "missing_tests_dir": [], @@ -157,6 +157,17 @@ "test_files": 1, "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", "path": "qa/audit_4big", @@ -248,9 +259,9 @@ ], "under_threshold": [], "totals": { - "suites": 20, - "test_files": 20, - "test_methods": 503, + "suites": 21, + "test_files": 21, + "test_methods": 534, "min_methods": 10, "under_threshold": 0 },