[DTP-Worker] Sprint 8 · buffer · RBAC/SPEC §3 : correction DÉFAUT FACTUEL (direction 10→9 · plateforme 5→6) + gate d'identité « Nb rôles » par portail ancré sur rbac_50_roles.json

La table §3 de RBAC_50_ROLES_SPEC.md avait dérivé du contrat en préservant le total
50 (10+5 vs 9+6) → invisible aux gates agrégés. Correction alignée sur la source +
nouveau bloc check_readme_claims recomptant chaque portail (et le total) depuis
rbac_50_roles.json. quality_report.json régénéré (evidence octets SPEC). 7 gates PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 09:39:54 +00:00
parent 5677082965
commit 7ce0f7b1e7
4 changed files with 86 additions and 3 deletions
+43
View File
@@ -826,6 +826,49 @@ if rp_man is not None and rp_rd is not None:
f"{mp.group(1)}/{mp.group(2)}/{mp.group(3)} MAIS MANIFEST dit "
f"{cp}/{cm}/{ct} → régénérer le README")
# 1quater) RBAC_50_ROLES_SPEC.md §3 — table « Nb rôles » PAR portail ------------
# La table §3 « Cartographie portails ↔ familles » transcrit À LA MAIN le nombre de
# rôles de CHAQUE portail. C'est une ventilation par portail dans un doc DISTINCT
# du MANIFEST roleprofile (gaté ci-dessus) — jamais gaté, et il AVAIT dérivé en
# silence : `direction` 10 / `plateforme` 5 alors que le contrat compte 9 / 6, le
# total 50 restant préservé (dérive muette classique, #6). On RECOMPTE ici chaque
# ligne depuis rbac_50_roles.json (source de vérité, byte-gatée par check_artifacts)
# et on exige l'égalité avec ce que la SPEC écrit, total compris. Un portail manquant
# de la table échoue aussi (disparition = régression de traçabilité).
SPEC_RD = f"{D}/rbac/RBAC_50_ROLES_SPEC.md"
try:
src_roles = load("rbac/rbac_50_roles.json")["roles"]
except (OSError, KeyError, ValueError) as e:
bad(f"RBAC SPEC §3 · rbac_50_roles.json illisible : {e}"); src_roles = None
spec_rd = read_norm(SPEC_RD)
if src_roles is not None and spec_rd is not None:
by_portail = {}
for r in src_roles:
by_portail[r.get("portail")] = by_portail.get(r.get("portail"), 0) + 1
for portail in sorted(by_portail):
exp = str(by_portail[portail])
pat = rf"\| \*\*{re.escape(portail)}\*\*[^|]*\| [^|]*\| (\d+) \|"
m = re.search(pat, spec_rd)
if m is None:
bad(f"RBAC SPEC §3 · portail `{portail}` — ligne INTROUVABLE dans "
f"{SPEC_RD} (attendu Nb rôles={exp})")
elif m.group(1) == exp:
good(f"RBAC SPEC §3 · portail `{portail}` — Nb rôles {m.group(1)} == "
f"rbac_50_roles.json ({exp})")
else:
bad(f"RBAC SPEC §3 · portail `{portail}` — {SPEC_RD} dit {m.group(1)} "
f"MAIS rbac_50_roles.json compte {exp} → corriger la SPEC")
exp_tot = str(len(src_roles))
mt = re.search(r"\*\*Total\*\* \| \*\*(\d+)\*\*", spec_rd)
if mt is None:
bad(f"RBAC SPEC §3 · total — ligne « Total » INTROUVABLE dans {SPEC_RD} "
f"(attendu {exp_tot})")
elif mt.group(1) == exp_tot:
good(f"RBAC SPEC §3 · total — {mt.group(1)} == rbac_50_roles.json ({exp_tot})")
else:
bad(f"RBAC SPEC §3 · total — {SPEC_RD} dit {mt.group(1)} MAIS "
f"rbac_50_roles.json compte {exp_tot} → corriger la SPEC")
# 2) apply_plan — ligne « Génération réelle » agrégeant les 3 volets ------------
AP_RD = f"{D}/rbac/apply_plan/README.md"
try: