[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:
@@ -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()
|
||||
Reference in New Issue
Block a user