[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,19 +1,21 @@
"""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
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).
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.
spec + roadmap 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 atexit
import copy
import os
import sys
import tempfile
import unittest
_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)}
# --------------------------------------------------------------------------- #
# 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:
return {"a/one": "S2", "b/two": "S3"}
@@ -48,23 +73,23 @@ def _mini_spec() -> dict:
return {
"version": "test",
"reference_cadre": "fixture",
"roadmap_ref": "ROADMAP.md",
"roadmap_ref": _ROADMAP_PATH,
"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",
"roadmap_line": _ROADMAP_S2, "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",
"roadmap_line": _ROADMAP_S3, "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",
"roadmap_line": _ROADMAP_M1, "statement": "métrique M1", "status": "in_repo",
"evidence_modules": ["a/one"], "evidence_artifacts": [],
"out_of_scope": []},
],
@@ -279,6 +304,63 @@ class TestNegativeInjections(unittest.TestCase):
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):
"""Le CLI build/validate produit les artefacts et sort proprement."""