Files
Claude Code DTP Worker 25541c365d [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>
2026-07-31 06:12:15 +00:00

125 lines
5.0 KiB
Python

"""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 re
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
# qa/acceptance/acclib → 05_deliverables_mvp
_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")
_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"]}
# 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:
"""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",
"parse_roadmap_anchors",
"artifact_exists",
"DELIVERABLES_ROOT",
]