Files
oto-enterprise-os-dtp/05_deliverables_mvp/qa/audit_4big/q4lib/registry.py
T
Claude Code DTP Worker a517619432 [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>
2026-07-30 10:11:02 +00:00

108 lines
3.8 KiB
Python

"""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,
}