[DTP-Worker] Sprint 7 · Générateur Audit 4Big qualité (95+/100 sur 100% deliverables) (QA · roadmap L69)
Audit de méta-niveau + gate : note la qualité 4Big de 100% des livrables gated et bloque (FAIL) si un module < 95/100 (CLAUDE.md #5). Couverture PROUVÉE par recoupement bijectif registre ↔ working-directory du CI (moins l'auditeur · SoD ISA 315). 5 critères déterministes (DOC/CONTRAT/TESTS/CLI/HANDOFF) renormalisés par archétype. Anti-invention (#6) : chaque note est recalculée depuis des faits du dépôt, jamais saisie ; un invariant recompute chaque note. Résultat : PASS · 17/17 modules à 100/100. Régression 442 tests verts (+34). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""Registre des modules audités + PREUVE de couverture 100% vs le CI Gitea.
|
||||
|
||||
Le périmètre revendiqué par cet audit — « 100% des livrables » — n'est crédible
|
||||
que s'il est PROUVÉ, pas déclaré. On lit donc `.gitea/workflows/ci.yml` (la seule
|
||||
autorité sur ce qui est réellement gated) et on vérifie :
|
||||
|
||||
1. l'ensemble des modules du registre == l'ensemble des `working-directory:`
|
||||
sous `05_deliverables_mvp/` du CI, PRIVÉ du module auditeur lui-même
|
||||
(séparation des pouvoirs · l'auditeur ne s'auto-note pas · ISA 315) ;
|
||||
2. chaque module gated alimente bien le job d'agrégat `gate` (via `needs`).
|
||||
|
||||
Ainsi, tout futur livrable ajouté au CI sans mise à jour du registre casse la
|
||||
couverture → le gate rougit. Aucune omission silencieuse possible.
|
||||
|
||||
Parsing YAML volontairement minimal (stdlib pure · aucune dépendance pip · #2).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from .deps import DELIVERABLES_ROOT
|
||||
|
||||
_REPO_ROOT = os.path.normpath(os.path.join(DELIVERABLES_ROOT, ".."))
|
||||
_CI_PATH = os.path.join(_REPO_ROOT, ".gitea", "workflows", "ci.yml")
|
||||
|
||||
_PREFIX = "05_deliverables_mvp/"
|
||||
|
||||
_JOB_RE = re.compile(r"^ ([A-Za-z0-9_-]+):\s*$")
|
||||
_WD_RE = re.compile(r"^\s*working-directory:\s*(\S+)\s*$")
|
||||
_NEEDS_RE = re.compile(r"^\s*needs:\s*\[(.*)\]\s*$")
|
||||
|
||||
|
||||
def _read_ci() -> str:
|
||||
with open(_CI_PATH, encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def parse_ci() -> dict:
|
||||
"""Retourne {job_id -> chemin module} pour les jobs sous 05_deliverables_mvp
|
||||
et la liste `needs` du job `gate`."""
|
||||
lines = _read_ci().splitlines()
|
||||
current_job = None
|
||||
job_to_path: dict[str, str] = {}
|
||||
gate_needs: list[str] = []
|
||||
in_gate = False
|
||||
|
||||
for line in lines:
|
||||
m = _JOB_RE.match(line)
|
||||
if m:
|
||||
current_job = m.group(1)
|
||||
in_gate = current_job == "gate"
|
||||
continue
|
||||
wd = _WD_RE.match(line)
|
||||
if wd and current_job:
|
||||
val = wd.group(1)
|
||||
if val.startswith(_PREFIX):
|
||||
job_to_path[current_job] = val[len(_PREFIX):]
|
||||
continue
|
||||
if in_gate:
|
||||
nm = _NEEDS_RE.match(line)
|
||||
if nm:
|
||||
gate_needs = [j.strip() for j in nm.group(1).split(",") if j.strip()]
|
||||
|
||||
return {"job_to_path": job_to_path, "gate_needs": gate_needs}
|
||||
|
||||
|
||||
def ci_module_paths(exclude: str | None = None) -> set[str]:
|
||||
"""Chemins de modules gated par le CI (hors `exclude`, ex. l'auditeur)."""
|
||||
paths = set(parse_ci()["job_to_path"].values())
|
||||
if exclude is not None:
|
||||
paths.discard(exclude)
|
||||
return paths
|
||||
|
||||
|
||||
def coverage_report(spec: dict) -> dict:
|
||||
"""Compare le registre du spec à la réalité du CI. Retourne un diagnostic
|
||||
sérialisable ; `ok=True` ssi couverture bijective + tous gated."""
|
||||
self_module = spec["self_module"]
|
||||
registry_paths = {m["path"] for m in spec["modules"]}
|
||||
ci = parse_ci()
|
||||
job_to_path = ci["job_to_path"]
|
||||
ci_paths = {p for p in job_to_path.values() if p != self_module}
|
||||
|
||||
missing_in_registry = sorted(ci_paths - registry_paths) # gated mais non audité
|
||||
missing_in_ci = sorted(registry_paths - ci_paths) # audité mais non gated
|
||||
|
||||
# Chaque module audité doit alimenter le gate d'agrégat.
|
||||
path_to_jobs: dict[str, list[str]] = {}
|
||||
for job, path in job_to_path.items():
|
||||
path_to_jobs.setdefault(path, []).append(job)
|
||||
not_in_gate = sorted(
|
||||
p for p in registry_paths
|
||||
if not any(j in ci["gate_needs"] for j in path_to_jobs.get(p, []))
|
||||
)
|
||||
|
||||
ok = not missing_in_registry and not missing_in_ci and not not_in_gate
|
||||
return {
|
||||
"ok": ok,
|
||||
"ci_modules_count": len(ci_paths),
|
||||
"registry_modules_count": len(registry_paths),
|
||||
"missing_in_registry": missing_in_registry,
|
||||
"missing_in_ci": missing_in_ci,
|
||||
"not_in_gate": not_in_gate,
|
||||
"self_module_excluded": self_module,
|
||||
}
|
||||
Reference in New Issue
Block a user