[DTP-Worker] Sprint 8 · buffer L75 · Traçabilité non vérifiée : les n° de ligne roadmap_line de la matrice d'acceptation pouvaient pointer à côté en silence (roadmap éditée) → INV11 ancrage roadmap (dérive 8+7 du fichier · #6) + parse_roadmap_anchors + 6 tests ; régénéré 558→564

INV10 ne garantissait que « roadmap_line est un entier positif ». Ajout de
parse_roadmap_anchors (deps) qui DÉRIVE la structure réelle de la roadmap, et
d'INV11 qui exige que chaque roadmap_line pointe RÉELLEMENT son bullet
(DELIVERABLE du sprint SX · k-ième bullet métrique) et que le « 8 + 7 » soit
dérivé du fichier, pas figé. Morsure prouvée sur le spec réel (S1=999, M3=200).
Régénéré consommateurs : regression 558→564 (run/plan/MANIFEST), quality_report
(acceptance 31→37 méthodes, 100/100 inchangé), fiches QA + Backend, README
acceptance (10→11 invariants). 7 gates verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-31 06:12:15 +00:00
parent 11f1250b29
commit 25541c365d
11 changed files with 273 additions and 22 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse.
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent** vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60 leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
de régression du repo : **558 tests · 22 suites** · verdict `PASS`, source de régression du repo : **564 tests · 22 suites** · verdict `PASS`, source
`qa/regression/out/regression_run.json` — jamais compté à la main · #6). `qa/regression/out/regression_run.json` — jamais compté à la main · #6).
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre) ## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
+1 -1
View File
@@ -32,7 +32,7 @@ périmètre — un auditeur ne s'audite jamais.
**conformité** normative des hand-off. **conformité** normative des hand-off.
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`) ## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
**22 suites gated · 558 tests · 558 passés · 0 échec · 0 erreur → verdict `PASS`.** **22 suites gated · 564 tests · 564 passés · 0 échec · 0 erreur → verdict `PASS`.**
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`, Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
réutilisé sans duplication) — jamais compté à la main. réutilisé sans duplication) — jamais compté à la main.
+66
View File
@@ -667,3 +667,69 @@ l'artefact.
**Hors périmètre worker (VPS · #8)** : néant (docs + gate bash/stdlib en-repo). **Hors périmètre worker (VPS · #8)** : néant (docs + gate bash/stdlib en-repo).
**Auto-score 4Big** : 96/100. **Auto-score 4Big** : 96/100.
---
## Session `20260731_060012` · Buffer S8 · Traçabilité NON vérifiée : les n° de ligne `roadmap_line` de la matrice d'acceptation pouvaient pointer à côté en silence
**Tâche** : **Sprint 8 · buffer L75 (« Regression tests exhaustifs »)** — roadmap
fonctionnellement close (22/22 modules gated · 558 tests · 7 gates verts au démarrage).
Recherche d'un **défaut réel** (traçabilité `CLAUDE.md` #6) plutôt qu'un cosmétique.
### Défaut trouvé — `qa/acceptance` : `roadmap_line` déclaré, jamais ancré
- **Symptôme** : chaque promesse de `acceptance_spec.json` (8 livrables S1..S8 + 7
métriques M1..M7) porte un `roadmap_line` (33, 39, … 87) censé pointer son bullet
dans `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`. Le docstring promet des « CITATIONS
verbatim de la roadmap » (#6), mais le **seul** invariant (INV10) ne vérifiait que
« `roadmap_line` est un entier positif ».
- **Contradiction** : si la roadmap est éditée (une ligne insérée/retirée, un sprint
ré-ordonné), **tous** les `roadmap_line` se décalent en silence — ils pointeraient un
bullet qui n'existe plus, tout en restant « vert ». Même classe de « vert trompeur »
que les compteurs périmés (534→551→558) ou l'`orphan_tests_dirs`, appliquée cette
fois à une **référence de ligne** (traçabilité déclarée mais non prouvée).
- **Vérif (#1 analyser)** : recomputé la structure réelle → 8 bullets `**DELIVERABLE**`
(S1@33 … S8@76) + 7 bullets métrique `- ✅` (81..87). Les `roadmap_line` du spec sont
**exacts aujourd'hui** — la faille est l'absence de garde, pas une valeur fausse.
### Fix (2 volets · vérifier + garder)
1. **`acclib/deps.py`** — nouveau `parse_roadmap_anchors(roadmap_ref)` qui **DÉRIVE**
la structure de la roadmap (jamais une liste de n° saisie · #6) : `{SX → ligne du
DELIVERABLE}` (SX **lu** dans le titre `## SPRINT n · SX (…`) + lignes ordonnées des
bullets métrique. `OSError` honnête si la roadmap est absente (référence fantôme = red).
2. **`acceptance_gen.py` · INV11 (ancrage roadmap)** — exige que (a) le nombre de bullets
DELIVERABLE == nb de livrables du spec et nb de bullets métrique == nb de métriques
(le **« 8 + 7 » devient dérivé du fichier**, plus un compte figé), (b) chaque livrable
SX pointe le DELIVERABLE de SX, (c) chaque métrique (dans l'ordre) pointe le k-ième
bullet `- ✅`. Docstring #6 mis à jour (« n° de ligne VÉRIFIÉ, pas déclaré »).
3. **Tests** — fixture roadmap **synthétique** (temp file, mêmes motifs en miniature :
S2@L3, S3@L5, M1@L7) pour contrôler l'écart injecté ; classe `TestRoadmapAnchor`
(+6 méthodes) : parse réel (8+7), DELIVERABLE désancré, métrique décalée, mauvais
propriétaire de sprint, compte en trop, roadmap absente. **37 méthodes** (31→37).
### Preuves
- **Passe** : build/validate réel **vert** ; `parse_roadmap_anchors` réel = {S1:33 …
S8:76} + métriques [81..87] ; les 15 `roadmap_line` du spec **matchent**.
- **Morsure (spec RÉEL)** : `S1.roadmap_line=999` ⇒ `roadmap: S1: … ne pointe pas le
DELIVERABLE de S1 (ligne 33)` ; `M3=200` ⇒ `… ≠ 3e bullet métrique (ligne 83)`.
**Régénération des consommateurs (mémoire projet)** : +6 méthodes test dans
`qa/acceptance` → matrice **558→564** (22 suites inchangées). Régénérés :
`qa/regression/out/{regression_run,regression_plan,MANIFEST}.json` (564/564 · PASS),
`qa/audit_4big/out/quality_report.json` (evidence acceptance « 31→37 méthodes », score
**100/100 inchangé**), fiches **QA** et **Backend** (`558→564`, gate `check_readme_claims`
vert), README acceptance (« 10→11 invariants »). L'artefact `acceptance_matrix.json` est
**byte-identique** (INV11 ne change que la validation, pas la sortie).
**Vérifs** : **7 gates** locaux verts (`guard_constraints` · `validate_json` ·
`check_docs` · `check_artifacts` · `check_regression` · `check_ci_integrity` ·
`check_readme_claims`) ; matrice **564/22 · PASS** ; 22/22 modules **100/100**.
**Anti-invention (#6)** : rien inventé — INV11 **dérive** la vérité de la roadmap et
compare ; le « 8 + 7 » cesse d'être un compte à la main.
**Hors périmètre worker (VPS · #8)** : néant (stdlib pur en-repo).
**Auto-score 4Big** : 96/100.
+6 -2
View File
@@ -43,7 +43,7 @@ qu'aucune promesse n'est silencieusement oubliée.
```bash ```bash
python3 acceptance_gen.py build # écrit out/acceptance_matrix.json + out/MANIFEST.json 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 acceptance_gen.py validate # (re)génère en mémoire + valide (schéma + 11 invariants)
python3 -m unittest discover -s tests -p 'test_*.py' -v python3 -m unittest discover -s tests -p 'test_*.py' -v
``` ```
@@ -56,7 +56,7 @@ python3 -m unittest discover -s tests -p 'test_*.py' -v
Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage). Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
## Invariants (10 familles) ## Invariants (11 familles)
1. ids uniques ; livrables S1..S8 + métriques M1..M7 ordonnés. 1. ids uniques ; livrables S1..S8 + métriques M1..M7 ordonnés.
2. toute preuve module est **gated** (drapeau recalculé depuis le CI). 2. toute preuve module est **gated** (drapeau recalculé depuis le CI).
@@ -68,6 +68,10 @@ Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
8. `mvp_metric` ⇒ sprint nul ; `sprint_deliverable` ⇒ sprint renseigné. 8. `mvp_metric` ⇒ sprint nul ; `sprint_deliverable` ⇒ sprint renseigné.
9. **SoD** : la matrice ne se cite jamais elle-même comme preuve. 9. **SoD** : la matrice ne se cite jamais elle-même comme preuve.
10. `roadmap_line` entier positif. 10. `roadmap_line` entier positif.
11. **ancrage roadmap** : chaque `roadmap_line` pointe RÉELLEMENT son bullet dans
`04_roadmap/…` (n° DELIVERABLE du sprint SX · k-ième bullet métrique), et le
compte « 8 + 7 » est **dérivé du fichier roadmap**, pas figé — une promesse
ajoutée/retirée ou des lignes décalées rougissent au lieu de dériver en silence.
## Hors périmètre worker (#8) ## Hors périmètre worker (#8)
@@ -21,7 +21,9 @@ module tracé non gated → génération refusée.
Anti-invention (#6) : les nombres des énoncés (<1h, 95/100, 7 dashboards, 2 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 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. mesure de performance ; tout hors-périmètre porte une source. Le n° de ligne
(`roadmap_line`) de chaque promesse est VÉRIFIÉ (INV11) contre la structure réelle
de la roadmap — il ne peut pas pointer à côté en silence si la roadmap est éditée.
Produit : Produit :
- acceptance_matrix.json → la matrice (une ligne par promesse : preuves - acceptance_matrix.json → la matrice (une ligne par promesse : preuves
@@ -218,6 +220,51 @@ def validate_bundle(bundle: dict, spec: dict, ci: dict, audit: dict) -> list[str
if any(not isinstance(x, int) or x < 1 for x in lines): if any(not isinstance(x, int) or x < 1 for x in lines):
errors.append(f"roadmap_line: valeur non positive {lines}.") errors.append(f"roadmap_line: valeur non positive {lines}.")
# INV11 · ANCRAGE ROADMAP (traçabilité VÉRIFIÉE, pas déclarée · #6). INV10 ne
# garantit que « roadmap_line est un entier » : un n° de ligne qui pointe à
# côté (roadmap éditée, lignes décalées) reste « vert » alors qu'il cite un
# bullet qui n'existe plus. On DÉRIVE la structure réelle de la roadmap et on
# exige que chaque roadmap_line pointe RÉELLEMENT le bullet qu'il prétend :
# · nombre de bullets DELIVERABLE == nombre de livrables de sprint du spec,
# et nombre de bullets métrique == nombre de métriques (le « 8 + 7 » n'est
# plus un compte figé mais dérivé du fichier roadmap) ;
# · chaque livrable SX pointe le bullet **DELIVERABLE** du sprint SX ;
# · chaque métrique (dans l'ordre) pointe le k-ième bullet « - ✅ ».
try:
anchors = deps.parse_roadmap_anchors(spec["roadmap_ref"])
except OSError as e:
errors.append(f"roadmap: {spec.get('roadmap_ref')!r} illisible ({e}).")
else:
dmap = anchors["deliverables"]
specd = spec.get("sprint_deliverables", [])
specm = spec.get("mvp_metrics", [])
if len(dmap) != len(specd):
errors.append(
f"roadmap: {len(dmap)} bullet(s) DELIVERABLE dans la roadmap ≠ "
f"{len(specd)} livrable(s) de sprint du spec (promesse ajoutée/retirée non tracée).")
if len(anchors["metrics"]) != len(specm):
errors.append(
f"roadmap: {len(anchors['metrics'])} bullet(s) métrique dans la roadmap ≠ "
f"{len(specm)} métrique(s) du spec (promesse ajoutée/retirée non tracée).")
for d in specd:
want = dmap.get(d["sprint"])
if want is None:
errors.append(
f"roadmap: {d['id']}: aucun bullet DELIVERABLE pour le sprint {d['sprint']}.")
elif d["roadmap_line"] != want:
errors.append(
f"roadmap: {d['id']}: roadmap_line={d['roadmap_line']} ne pointe pas "
f"le DELIVERABLE de {d['sprint']} (ligne {want}).")
for k, d in enumerate(specm):
want = anchors["metrics"][k] if k < len(anchors["metrics"]) else None
if want is None:
errors.append(
f"roadmap: {d['id']}: aucun {k + 1}e bullet métrique dans la roadmap.")
elif d["roadmap_line"] != want:
errors.append(
f"roadmap: {d['id']}: roadmap_line={d['roadmap_line']}{k + 1}e bullet "
f"métrique (ligne {want}).")
return errors return errors
@@ -22,11 +22,14 @@ from __future__ import annotations
import json import json
import os import os
import re
import sys import sys
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
# qa/acceptance/acclib → 05_deliverables_mvp # qa/acceptance/acclib → 05_deliverables_mvp
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", "..")) _DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
# 05_deliverables_mvp → racine du dépôt (où vit 04_roadmap/).
_REPO_ROOT = os.path.normpath(os.path.join(_DELIVERABLES, ".."))
_PUB = os.path.join(_DELIVERABLES, "publiciste") _PUB = os.path.join(_DELIVERABLES, "publiciste")
_AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big") _AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big")
@@ -55,6 +58,54 @@ def audit_sprints() -> dict:
return {m["path"]: m["sprint"] for m in spec["modules"]} return {m["path"]: m["sprint"] for m in spec["modules"]}
# Ancres de traçabilité de la roadmap (structure, jamais des n° saisis à la main).
_SPRINT_HEAD = re.compile(r"##\s+SPRINT\s+\d+.*?·\s*(S\d+)\s*\(")
_METRIC_HEAD = re.compile(r"##\s+M[ée]triques\s+succ")
_DELIVERABLE = re.compile(r"\*\*DELIVERABLE")
_METRIC_BULLET = re.compile(r"^-\s*✅")
def parse_roadmap_anchors(roadmap_ref: str) -> dict:
"""DÉRIVE les ancres de traçabilité depuis la roadmap réelle (#6 · jamais une
liste de n° de ligne saisie à la main).
Renvoie :
- ``deliverables`` : {sprint SX -> n° de ligne du bullet ``**DELIVERABLE**``
appartenant au sprint SX} (le SX est LU dans le titre ``## SPRINT n … · SX (…``) ;
- ``metrics`` : [n° de ligne] des bullets ``- ✅`` sous la section
``## Métriques succès MVP``, en ordre de fichier ;
- ``path`` : le chemin résolu (pour messages).
``roadmap_ref`` absolu est utilisé tel quel ; relatif est résolu depuis la
racine du dépôt. Lève ``OSError`` si la roadmap est absente/illisible — une
référence de traçabilité vers un fichier fantôme est un mensonge (red honnête)."""
path = roadmap_ref if os.path.isabs(roadmap_ref) \
else os.path.join(_REPO_ROOT, roadmap_ref)
with open(path, encoding="utf-8") as fh:
lines = fh.read().splitlines()
deliverables: dict[str, int] = {}
metrics: list[int] = []
current_sprint: str | None = None
in_metrics = False
for i, ln in enumerate(lines, 1):
mh = _SPRINT_HEAD.match(ln)
if mh:
current_sprint, in_metrics = mh.group(1), False
continue
if _METRIC_HEAD.match(ln):
current_sprint, in_metrics = None, True
continue
if ln.startswith("## "): # toute autre section clôt le contexte
current_sprint, in_metrics = None, False
continue
if current_sprint and _DELIVERABLE.search(ln):
deliverables[current_sprint] = i
if in_metrics and _METRIC_BULLET.match(ln):
metrics.append(i)
return {"deliverables": deliverables, "metrics": metrics, "path": path}
def artifact_exists(rel_path: str) -> bool: def artifact_exists(rel_path: str) -> bool:
"""Vrai ssi l'artefact cité (chemin relatif à 05_deliverables_mvp) existe. """Vrai ssi l'artefact cité (chemin relatif à 05_deliverables_mvp) existe.
@@ -67,6 +118,7 @@ __all__ = [
"validate", "validate",
"parse_ci", "parse_ci",
"audit_sprints", "audit_sprints",
"parse_roadmap_anchors",
"artifact_exists", "artifact_exists",
"DELIVERABLES_ROOT", "DELIVERABLES_ROOT",
] ]
@@ -1,19 +1,21 @@
"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip). """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 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 conformité au schéma, les 11 familles d'invariants, le déterminisme, et une
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole). 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 + 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 spec + roadmap factices) pour contrôler exactement l'écart injecté, sans dépendre
forme du dépôt réel. de la forme du dépôt réel.
""" """
from __future__ import annotations from __future__ import annotations
import atexit
import copy import copy
import os import os
import sys import sys
import tempfile
import unittest import unittest
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
@@ -40,6 +42,29 @@ def _mini_ci(paths, self_module=_SELF) -> dict:
return {"job_to_path": j2p, "gate_needs": list(j2p)} return {"job_to_path": j2p, "gate_needs": list(j2p)}
# --------------------------------------------------------------------------- #
# Roadmap SYNTHÉTIQUE : la même structure que la vraie (## SPRINT n · SX · bullet
# **DELIVERABLE**, puis section Métriques avec bullets « - ✅ »), en miniature.
# INV11 lit ce fichier ; on contrôle donc exactement les n° de ligne attendus :
# L3 → DELIVERABLE S2 · L5 → DELIVERABLE S3 · L7 → 1er bullet métrique (M1).
# --------------------------------------------------------------------------- #
_MINI_ROADMAP = (
"# Roadmap fixture\n" # L1
"## SPRINT 2 · Fixture · S2 (obj S1)\n" # L2
"- **DELIVERABLE** : livrable S2\n" # L3
"## SPRINT 3 · Fixture · S3 (obj S2)\n" # L4
"- **DELIVERABLE** : livrable S3\n" # L5
"## Métriques succès MVP (fixture)\n" # L6
"- ✅ métrique M1\n" # L7
)
_ROADMAP_S2, _ROADMAP_S3, _ROADMAP_M1 = 3, 5, 7
_fd, _ROADMAP_PATH = tempfile.mkstemp(suffix="_roadmap.md", prefix="acc_fixture_")
with os.fdopen(_fd, "w", encoding="utf-8") as _fh:
_fh.write(_MINI_ROADMAP)
atexit.register(lambda: os.path.exists(_ROADMAP_PATH) and os.remove(_ROADMAP_PATH))
def _mini_audit() -> dict: def _mini_audit() -> dict:
return {"a/one": "S2", "b/two": "S3"} return {"a/one": "S2", "b/two": "S3"}
@@ -48,23 +73,23 @@ def _mini_spec() -> dict:
return { return {
"version": "test", "version": "test",
"reference_cadre": "fixture", "reference_cadre": "fixture",
"roadmap_ref": "ROADMAP.md", "roadmap_ref": _ROADMAP_PATH,
"self_module": _SELF, "self_module": _SELF,
"cible_portage": "recette", "cible_portage": "recette",
"extra_module_sprint": {}, "extra_module_sprint": {},
"sprint_deliverables": [ "sprint_deliverables": [
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2", {"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
"roadmap_line": 39, "statement": "livrable S2", "status": "in_repo", "roadmap_line": _ROADMAP_S2, "statement": "livrable S2", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [], "evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": []}, "out_of_scope": []},
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3", {"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
"roadmap_line": 46, "statement": "livrable S3", "status": "in_repo", "roadmap_line": _ROADMAP_S3, "statement": "livrable S3", "status": "in_repo",
"evidence_modules": ["b/two"], "evidence_artifacts": [], "evidence_modules": ["b/two"], "evidence_artifacts": [],
"out_of_scope": []}, "out_of_scope": []},
], ],
"mvp_metrics": [ "mvp_metrics": [
{"id": "M1", "kind": "mvp_metric", "sprint": None, {"id": "M1", "kind": "mvp_metric", "sprint": None,
"roadmap_line": 81, "statement": "métrique M1", "status": "in_repo", "roadmap_line": _ROADMAP_M1, "statement": "métrique M1", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [], "evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": []}, "out_of_scope": []},
], ],
@@ -279,6 +304,63 @@ class TestNegativeInjections(unittest.TestCase):
self.assertFalse(b["verdict"]) self.assertFalse(b["verdict"])
# --------------------------------------------------------------------------- #
class TestRoadmapAnchor(unittest.TestCase):
"""INV11 · les n° de ligne `roadmap_line` sont ANCRÉS à la structure réelle
de la roadmap (traçabilité vérifiée, pas déclarée)."""
def test_parse_real_roadmap_structure(self):
# Le parseur dérive 8 livrables (S1..S8) + 7 métriques de la vraie roadmap.
spec = _spec()
a = deps.parse_roadmap_anchors(spec["roadmap_ref"])
self.assertEqual(sorted(a["deliverables"]),
[f"S{i}" for i in range(1, 9)])
self.assertEqual(len(a["metrics"]), 7)
# Chaque roadmap_line du spec réel pointe bien son bullet.
for d in spec["sprint_deliverables"]:
self.assertEqual(d["roadmap_line"], a["deliverables"][d["sprint"]],
f"{d['id']} désancré")
for k, d in enumerate(spec["mvp_metrics"]):
self.assertEqual(d["roadmap_line"], a["metrics"][k], f"{d['id']} désancré")
def test_deliverable_line_offby_fails(self):
spec = _mini_spec()
spec["sprint_deliverables"][0]["roadmap_line"] = 99 # ne pointe plus S2
self.assertTrue(any("ne pointe pas" in e for e in _mini_errors(spec)),
_mini_errors(spec))
def test_metric_line_offby_fails(self):
spec = _mini_spec()
spec["mvp_metrics"][0]["roadmap_line"] = 1 # pas le bullet métrique
self.assertTrue(any("bullet métrique" in e for e in _mini_errors(spec)),
_mini_errors(spec))
def test_wrong_sprint_owner_fails(self):
# Le livrable prétend S3 mais garde la ligne du DELIVERABLE de S2.
spec = _mini_spec()
spec["sprint_deliverables"][0]["sprint"] = "S3"
spec["sprint_deliverables"][0]["roadmap_line"] = _ROADMAP_S2
self.assertTrue(any("ne pointe pas" in e for e in _mini_errors(spec)),
_mini_errors(spec))
def test_extra_deliverable_count_mismatch_fails(self):
# Un livrable de plus que ce que contient la roadmap fixture.
spec = _mini_spec()
spec["sprint_deliverables"].append({
"id": "S4", "kind": "sprint_deliverable", "sprint": "S4",
"roadmap_line": 3, "statement": "x", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [], "out_of_scope": []})
errs = _mini_errors(spec)
self.assertTrue(any("bullet(s) DELIVERABLE" in e for e in errs)
or any("aucun bullet DELIVERABLE" in e for e in errs), errs)
def test_missing_roadmap_file_fails(self):
spec = _mini_spec()
spec["roadmap_ref"] = "/nonexistent/path/ROADMAP.md"
self.assertTrue(any("illisible" in e for e in _mini_errors(spec)),
_mini_errors(spec))
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
class TestCLI(unittest.TestCase): class TestCLI(unittest.TestCase):
"""Le CLI build/validate produit les artefacts et sort proprement.""" """Le CLI build/validate produit les artefacts et sort proprement."""
@@ -858,7 +858,7 @@
"criterion": "DOC", "criterion": "DOC",
"weight": 20, "weight": 20,
"passed": true, "passed": true,
"evidence": "README.md (3714 octets)" "evidence": "README.md (4061 octets)"
}, },
{ {
"criterion": "CONTRAT", "criterion": "CONTRAT",
@@ -870,7 +870,7 @@
"criterion": "TESTS", "criterion": "TESTS",
"weight": 25, "weight": 25,
"passed": true, "passed": true,
"evidence": "31 méthodes test_* dans 1 fichier(s) (seuil 8)" "evidence": "37 méthodes test_* dans 1 fichier(s) (seuil 8)"
}, },
{ {
"criterion": "CLI", "criterion": "CLI",
@@ -5,7 +5,7 @@
"version": "1.0", "version": "1.0",
"verdict": "PASS", "verdict": "PASS",
"suites": 22, "suites": 22,
"test_methods": 558, "test_methods": 564,
"coverage_ok": true, "coverage_ok": true,
"artifacts": [ "artifacts": [
"regression_plan.json" "regression_plan.json"
@@ -178,7 +178,7 @@
"in_gate": true, "in_gate": true,
"has_tests_dir": true, "has_tests_dir": true,
"test_files": 1, "test_files": 1,
"test_methods": 31 "test_methods": 37
}, },
{ {
"id": "qa.audit_4big", "id": "qa.audit_4big",
@@ -273,7 +273,7 @@
"totals": { "totals": {
"suites": 22, "suites": 22,
"test_files": 22, "test_files": 22,
"test_methods": 558, "test_methods": 564,
"min_methods": 10, "min_methods": 10,
"under_threshold": 0 "under_threshold": 0
}, },
@@ -162,9 +162,9 @@
{ {
"id": "qa.acceptance", "id": "qa.acceptance",
"path": "qa/acceptance", "path": "qa/acceptance",
"expected_methods": 31, "expected_methods": 37,
"ran": 31, "ran": 37,
"passed": 31, "passed": 37,
"failures": 0, "failures": 0,
"errors": 0, "errors": 0,
"skipped": 0, "skipped": 0,
@@ -272,8 +272,8 @@
"suites": 22, "suites": 22,
"green": 22, "green": 22,
"red": 0, "red": 0,
"ran": 558, "ran": 564,
"passed": 558, "passed": 564,
"failures": 0, "failures": 0,
"errors": 0, "errors": 0,
"skipped": 0 "skipped": 0