Files
oto-enterprise-os-dtp/05_deliverables_mvp/qa/audit_4big/audit_4big_gen.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

219 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""Générateur de l'audit 4Big qualité · Sprint 7 · QA.
Roadmap Sprint 7 · QA : « Audit 4Big niveau 95+/100 sur 100% deliverables ».
Ce générateur est un audit de MÉTA-NIVEAU : il note la qualité 4Big de TOUS les
livrables du mandat à partir de FAITS du dépôt (documentation, contrat de sortie
schema, couverture de tests, CLI reproductible, intégrité du hand-off out/) et
exige une note ≥ 95/100 sur chacun (CLAUDE.md #5).
Le périmètre « 100% des livrables » est PROUVÉ, pas déclaré : le registre est
recoupé avec les `working-directory` du CI Gitea (moins l'auditeur lui-même,
séparation des pouvoirs · ISA 315). Toute divergence casse le verdict.
ANTI-INVENTION (#6) : aucune note n'est saisie à la main — chaque note est
RECALCULÉE depuis des faits vérifiables (présence de fichiers, taille, comptage
des méthodes test_*, validité JSON du hand-off). Un module dont un critère
applicable échoue tombe sous 95 → FAIL global (l'audit est un GATE).
Sous-commandes :
build [-o OUT] → écrit quality_report.json + MANIFEST.json
validate → (re)génère en mémoire, valide schéma + invariants ;
sort en erreur si un module < 95, si la couverture n'est
pas 100%, ou si un invariant casse.
Sortie déterministe (tri stable, aucun horodatage) → diffable + re-générable.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
from q4lib import builder, scoring # noqa: E402
from q4lib.deps import validate as maison_validate # noqa: E402
_SPEC_PATH = os.path.join(_HERE, "quality_spec.json")
_SCHEMA_PATH = os.path.join(_HERE, "quality.schema.json")
_DEFAULT_OUT = os.path.join(_HERE, "out")
_KNOWN_CRITERIA = {"DOC", "CONTRAT", "TESTS", "CLI", "HANDOFF"}
def _eprint(*args) -> None:
print(*args, file=sys.stderr)
def _load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def _write_json(path: str, data) -> None:
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
fh.write("\n")
def _manifest(spec: dict, report: dict) -> dict:
return {
"generated_from": "quality_spec.json",
"generator": "audit_4big_gen.py",
"audit": report["audit"],
"version": report["version"],
"verdict": report["verdict"],
"modules_audited": report["totals"]["modules"],
"min_score": report["totals"]["min_score"],
"pass_score": report["pass_score"],
"coverage_ok": report["coverage"]["ok"],
"artifacts": ["quality_report.json"],
"roadmap": "Sprint 7 · QA · Audit 4Big 95+/100 sur 100% deliverables",
"hors_perimetre_worker": (
"Publication du rapport dans le desk ERPNext + branchement du gate "
"4Big sur le pipeline de release VPS → agent QA / DevOps (#8)."
),
}
def check_invariants(report: dict, spec: dict) -> list[str]:
"""Invariants de cohérence 4Big. Retourne la liste des violations."""
errs: list[str] = []
# 1. Schéma de sortie.
schema = _load(_SCHEMA_PATH)
errs += [f"schema: {e}" for e in maison_validate(report, schema)]
# 2. Poids des critères = 100 (base avant renormalisation).
total_w = sum(c["weight"] for c in spec["criteria"])
if total_w != 100:
errs.append(f"INV2 somme des poids critères = {total_w} ≠ 100")
# 3. Archétypes bien formés (applicable non vide ⊂ critères connus).
for name, arch in spec["archetypes"].items():
appl = set(arch["applicable"])
if not appl:
errs.append(f"INV3 archétype {name} sans critère applicable")
if not appl <= _KNOWN_CRITERIA:
errs.append(f"INV3 archétype {name} critères inconnus "
f"{sorted(appl - _KNOWN_CRITERIA)}")
# 4. L'auditeur ne s'auto-audite pas.
self_mod = spec["self_module"]
if any(m["path"] == self_mod for m in report["modules"]):
errs.append(f"INV4 auditeur {self_mod} présent dans le périmètre audité")
# 5. Couverture 100% (bijection registre ↔ CI, tous dans le gate).
cov = report["coverage"]
if not cov["ok"]:
errs.append(
f"INV5 couverture incomplète : missing_in_registry="
f"{cov['missing_in_registry']} missing_in_ci={cov['missing_in_ci']} "
f"not_in_gate={cov['not_in_gate']}")
if not (cov["ci_modules_count"] == cov["registry_modules_count"]
== len(report["modules"]) == len(spec["modules"])):
errs.append("INV5 comptages couverture/registre/rapport incohérents")
# 6. Par module : recompute note + cohérence des checks avec l'archétype.
for m in report["modules"]:
appl = set(spec["archetypes"][m["archetype"]]["applicable"])
got = {c["criterion"] for c in m["checks"]}
if got != appl:
errs.append(f"INV6 {m['id']} checks {sorted(got)} ≠ applicables "
f"{sorted(appl)}")
wsum = sum(c["weight"] for c in m["checks"])
if wsum != m["applicable_weight"]:
errs.append(f"INV6 {m['id']} Σpoids checks {wsum} ≠ applicable_weight "
f"{m['applicable_weight']}")
earned = sum(c["weight"] for c in m["checks"] if c["passed"])
if earned != m["earned_weight"]:
errs.append(f"INV6 {m['id']} earned recomputé {earned}"
f"{m['earned_weight']}")
recomputed = scoring._round_half_up(100.0 * earned / m["applicable_weight"])
if recomputed != m["score"]:
errs.append(f"INV6 {m['id']} note recomputée {recomputed}"
f"{m['score']} (note fabriquée ?)")
# 7. Le gate : chaque module ≥ pass_score et verdict cohérent.
ps = spec["thresholds"]["pass_score"]
for m in report["modules"]:
expect = "PASS" if m["score"] >= ps else "FAIL"
if m["verdict"] != expect:
errs.append(f"INV7 {m['id']} verdict {m['verdict']} ≠ attendu {expect}")
if m["verdict"] != "PASS":
errs.append(f"INV7 {m['id']} note {m['score']} < seuil 4Big {ps}")
# 8. Totaux cohérents.
t = report["totals"]
scores = [m["score"] for m in report["modules"]]
if t["pass"] + t["fail"] != t["modules"]:
errs.append("INV8 pass+fail ≠ modules")
if scores and (t["min_score"] != min(scores) or t["max_score"] != max(scores)):
errs.append("INV8 min/max score incohérents")
# 9. Verdict global.
expect_global = "PASS" if (cov["ok"] and t["fail"] == 0) else "FAIL"
if report["verdict"] != expect_global:
errs.append(f"INV9 verdict global {report['verdict']}{expect_global}")
return errs
def cmd_build(args: argparse.Namespace) -> int:
spec = _load(_SPEC_PATH)
report = builder.build(spec)
errs = check_invariants(report, spec)
if errs:
_eprint("❌ Invariants violés — build refusé :")
for e in errs:
_eprint(f" · {e}")
return 1
out_dir = args.out or _DEFAULT_OUT
os.makedirs(out_dir, exist_ok=True)
_write_json(os.path.join(out_dir, "quality_report.json"), report)
_write_json(os.path.join(out_dir, "MANIFEST.json"), _manifest(spec, report))
print(f"✅ Audit 4Big écrit dans {out_dir}/ — verdict {report['verdict']} · "
f"{report['totals']['pass']}/{report['totals']['modules']} modules ≥ "
f"{report['pass_score']} (min {report['totals']['min_score']}).")
return 0
def cmd_validate(_args: argparse.Namespace) -> int:
spec = _load(_SPEC_PATH)
report = builder.build(spec)
errs = check_invariants(report, spec)
if errs:
_eprint("❌ Audit 4Big NON conforme :")
for e in errs:
_eprint(f" · {e}")
return 1
print(f"✅ Audit 4Big conforme — verdict {report['verdict']} · couverture "
f"{report['coverage']['registry_modules_count']} modules · min "
f"{report['totals']['min_score']}/100 (seuil {report['pass_score']}).")
return 0
def build_argparser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(prog="audit_4big_gen",
description=__doc__.splitlines()[0])
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("build", help="écrit quality_report.json + MANIFEST.json")
p.add_argument("-o", "--out", default=None, help="répertoire de sortie")
p.set_defaults(func=cmd_build)
p = sub.add_parser("validate", help="valide schéma + invariants (sans écrire)")
p.set_defaults(func=cmd_validate)
return ap
def main(argv: list[str] | None = None) -> int:
ns = build_argparser().parse_args(argv)
return ns.func(ns)
if __name__ == "__main__":
raise SystemExit(main())