[DTP-Worker] Sprint 8 · buffer L75 · Intégrité matrice régression : fix gate check_regression cassé en CI (run baseline commité) + invariant disque→CI (INV4 orphan_tests_dirs)

Défaut #1 (bug CI réel) : ci/check_regression.sh exigeait regression_run.json
commité comme baseline, mais le fichier était .gitignore → absent en checkout
propre → le job Gitea check-regression échouait sur CHAQUE run CI (ne passait
qu'en local via un fichier non suivi). L'artefact run est byte-déterministe
(aucun horodatage/hôte/durée · path relatif) → committer est licite. Fix :
run désormais commité (baseline), .gitignore + README réécrits.

Défaut #2 (invariant manquant) : coverage_report prouvait CI→disque mais pas
l'inverse. Nouveau disk_test_modules() + orphan_tests_dirs dans INV4 : un module
gagnant un tests/ sans job CI fait chuter la couverture (fini la sous-comptée
silencieuse de la matrice). Schéma + 2 tests de morsure.

Consommateur régénéré : qa/audit_4big/quality_report.json (README 3710→4236 o,
24→26 méthodes). Matrice 534/21/PASS inchangée (harnais self-exclu, INV3).
5 gates verts · 26+34 tests OK · YAML valide.
This commit is contained in:
Claude Code DTP Worker
2026-07-31 00:41:16 +00:00
parent 0eaf5bde39
commit 76fb0e768d
10 changed files with 399 additions and 56 deletions
@@ -47,6 +47,25 @@ def count_tests(tests_dir: str) -> tuple[int, int]:
return (files, methods)
def disk_test_modules() -> list[str]:
"""Chemins de modules (relatifs à DELIVERABLES_ROOT, POSIX) portant un
répertoire `tests/` sur DISQUE — indépendamment du CI.
Sert à prouver le sens INVERSE de la couverture : tout module qui gagne des
tests sur disque DOIT être câblé au CI, sinon ses tests ne tournent jamais et
n'entrent jamais dans la matrice (dérive silencieuse disque→CI). Faits de
disque purs, ordonnés → déterministe."""
mods: set[str] = set()
for dirpath, dirnames, _ in os.walk(DELIVERABLES_ROOT):
# On n'explore pas l'intérieur des caches/tests eux-mêmes.
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
if os.path.basename(dirpath) == "tests":
module_abs = os.path.dirname(dirpath)
rel = os.path.relpath(module_abs, DELIVERABLES_ROOT)
mods.add(rel.replace(os.sep, "/"))
return sorted(mods)
def discover_suites(spec: dict) -> list[dict]:
"""Liste ordonnée des suites gated (hors self), enrichie des faits de disque.
@@ -90,10 +109,15 @@ def coverage_report(spec: dict, suites: list[dict]) -> dict:
not_in_gate = sorted(s["path"] for s in suites if not s["in_gate"])
self_present = any(s["path"] == self_module for s in suites)
# Sens inverse : tout module portant un `tests/` sur disque DOIT être gated
# (sinon ses tests ne tournent jamais → dérive silencieuse disque→CI).
orphan_tests_dirs = sorted(set(disk_test_modules()) - ci_paths)
ok = (
not missing_tests_dir
and not not_in_gate
and not self_present
and not orphan_tests_dirs
and self_module in ci_paths # le harnais DOIT être gated lui aussi
)
return {
@@ -104,4 +128,5 @@ def coverage_report(spec: dict, suites: list[dict]) -> dict:
"self_module_gated": self_module in ci_paths,
"missing_tests_dir": missing_tests_dir,
"not_in_gate": not_in_gate,
"orphan_tests_dirs": orphan_tests_dirs,
}