[DTP-Worker] Sprint 8 · buffer L75 · Domaine QA/Audit 5D : la synthèse de l'audit de conformité (« 17 contrôles · 5 dimensions ») était transcrite À LA MAIN dans DEUX docs (README du module qa/audit_5d + cellule de la fiche agent qa, même couple du même catalogue de contrôles) sans AUCUN gate, et la ventilation du verdict (« 13 PASS · 0 FAIL · 4 à confirmer ») du seul README non plus — ajouter un contrôle/une dimension au catalogue OU faire basculer un contrôle en A_CONFIRMER ferait dériver les docs en silence pendant que les DEUX artefacts byte-gatés par check_artifacts (qa/audit_5d/out/MANIFEST.json = controls_total+dimensions · out/audit_report.json = totals.{pass,fail,a_confirmer}, recomptés du catalogue à chaque build) disent autre chose → nouveau bloc dans check_readme_claims recomputant le couple (contrôles+dimensions) depuis MANIFEST et la ventilation (pass/fail/à-confirmer) depuis audit_report.totals, exigeant l'égalité avec les deux docs + cohérence croisée controls_total==totals.controls (4 morsures vérifiées : dérive contrôles README · bascule 13→12/4→5 captée là où le total de contrôles reste 17 et resterait aveugle · dérive dimensions fiche agent · claim verdict disparu = red)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-31 11:06:21 +00:00
parent 8f27b65724
commit 9ac7bdb2de
3 changed files with 146 additions and 1 deletions
+81
View File
@@ -802,6 +802,87 @@ if conf_exp is not None:
bad(f"Legal confotur · {path_f} — dit {'/'.join(m.groups())} MAIS "
f"MANIFEST dit {'/'.join(conf_exp)} ({conf_src}) → régénérer le doc")
# ============================================================================
# Audit 5D · synthèse de l'audit de conformité (contrôles / dimensions / verdict
# pass-fail-à confirmer) — même classe de « vert trompeur », transcrite À LA MAIN
# dans DEUX docs.
# ----------------------------------------------------------------------------
# Le générateur audit_5d émet DEUX artefacts byte-gatés par check_artifacts
# (⇒ faisant autorité) : out/MANIFEST.json (controls_total, `dimensions`,
# `audited_artifacts`, `open_items`, verdict — recomptés du catalogue de contrôles
# à chaque build) ET out/audit_report.json dont `totals` porte la ventilation
# {controls, pass, fail, a_confirmer} recalculée en rejouant les 17 contrôles sur
# les hand-off amont. Deux nombres de cette synthèse sont recopiés À LA MAIN à
# DEUX endroits : la phrase d'ouverture du README du module (05_deliverables_mvp/
# qa/audit_5d/README.md — « **17 contrôles** répartis en **5 dimensions (5D)** »)
# ET la cellule de la fiche agent QA (03_agents/qa/AGENT.md — « 17 contrôles / 5
# dimensions »). La ventilation du verdict (« **13 PASS · 0 FAIL · 4 à confirmer** »)
# n'est portée QUE par le README, mais se PÉRIME dès qu'un contrôle bascule en
# A_CONFIRMER (pass 13→12, à confirmer 4→5) alors que le TOTAL de contrôles reste
# 17 — dérive AVEUGLE au seul compte de contrôles, d'où sa recomputation séparée
# depuis audit_report.totals. Aucun gate ne couvrait ces claims : ajouter un
# contrôle/une dimension au catalogue, ou faire basculer un contrôle, ferait
# dériver LES DEUX docs (et la ligne verdict) en silence pendant que les artefacts
# disent autre chose — même piège que les mots-clés SEO, les triplets par workspace
# des portails ou le quintuplet CONFOTUR. On RECOMPUTE ici (contrôles + dimensions
# depuis le MANIFEST · pass/fail/à confirmer depuis audit_report.totals · déjà
# byte-gatés · zéro duplication du compteur du générateur) et on exige l'égalité.
# Un claim absent échoue AUSSI (traçabilité). Cohérence croisée en bonus : le
# `controls_total` du MANIFEST doit égaler `totals.controls` du rapport.
# ============================================================================
A5D_RD = f"{D}/qa/audit_5d/README.md"
A5D_FI = "03_agents/qa/AGENT.md"
try:
a5_man = load("qa/audit_5d/out/MANIFEST.json")
a5_tot = load("qa/audit_5d/out/audit_report.json")["totals"]
a5_ctl = str(a5_man["controls_total"])
a5_dim = str(len(a5_man["dimensions"]))
if a5_ctl != str(a5_tot["controls"]):
bad(f"Audit 5D · incohérence artefacts : MANIFEST controls_total={a5_ctl} "
f"MAIS audit_report totals.controls={a5_tot['controls']}")
a5_pass, a5_fail = str(a5_tot["pass"]), str(a5_tot["fail"])
a5_conf = str(a5_tot["a_confirmer"])
except (OSError, KeyError, ValueError) as e:
bad(f"Audit 5D · artefact illisible/inattendu : {e}"); a5_ctl = None
if a5_ctl is not None:
a5_src = f"contrôles={a5_ctl} dimensions={a5_dim}"
# 1) README module — phrase d'ouverture (contrôles + dimensions)
a5_rd = read_norm(A5D_RD)
if a5_rd is not None:
m = re.search(r"\*\*(\d+) contrôles\*\* répartis en \*\*(\d+) dimensions \(5D\)\*\*",
a5_rd)
if m is None:
bad(f"Audit 5D · {A5D_RD} — synthèse contrôles/dimensions INTROUVABLE "
f"(attendu {a5_src})")
elif m.groups() == (a5_ctl, a5_dim):
good(f"Audit 5D · {A5D_RD} — {'/'.join(m.groups())} == artefact ({a5_src})")
else:
bad(f"Audit 5D · {A5D_RD} — dit {'/'.join(m.groups())} MAIS artefact dit "
f"{a5_ctl}/{a5_dim} ({a5_src}) → régénérer le README")
# 2) README module — ventilation du verdict (pass / fail / à confirmer)
a5_vsrc = f"pass={a5_pass} fail={a5_fail} à_confirmer={a5_conf}"
m = re.search(r"\*\*(\d+) PASS · (\d+) FAIL · (\d+) à confirmer\*\*", a5_rd)
if m is None:
bad(f"Audit 5D · {A5D_RD} — ventilation verdict INTROUVABLE (attendu {a5_vsrc})")
elif m.groups() == (a5_pass, a5_fail, a5_conf):
good(f"Audit 5D · {A5D_RD} — verdict {'/'.join(m.groups())} == "
f"audit_report.totals ({a5_vsrc})")
else:
bad(f"Audit 5D · {A5D_RD} — dit {'/'.join(m.groups())} MAIS totals disent "
f"{a5_pass}/{a5_fail}/{a5_conf} ({a5_vsrc}) → régénérer le README")
# 3) Fiche agent QA — même couple contrôles/dimensions, 2e transcription
a5_fi = read_norm(A5D_FI)
if a5_fi is not None:
m = re.search(r"(\d+) contrôles / (\d+) dimensions", a5_fi)
if m is None:
bad(f"Audit 5D · {A5D_FI} — claim « N contrôles / M dimensions » INTROUVABLE "
f"(attendu {a5_src})")
elif m.groups() == (a5_ctl, a5_dim):
good(f"Audit 5D · {A5D_FI} — {'/'.join(m.groups())} == artefact ({a5_src})")
else:
bad(f"Audit 5D · {A5D_FI} — dit {'/'.join(m.groups())} MAIS artefact dit "
f"{a5_ctl}/{a5_dim} ({a5_src}) → régénérer la fiche")
sys.exit(1 if FAIL else 0)
PY
rc=$?