[DTP-Worker 20260801_233454] Sprint 8 · buffer · RBAC SPEC §5 : gate de la LIGNE D'AUTO-CONTRÔLE du « résumé »

La SPEC RBAC a déjà 3 gates (§3 ventilation par portail · §4 colonne Entité · §7
run-book) mais son §5 « Les N rôles (résumé) » restait AVEUGLE. Ce §5 restitue le
total de rôles sur 3 surfaces prose : le compte-titre « ## 5. Les 50 rôles », NEUF
comptes parenthétiques de regroupement (`**Ventes (8)**` …) et une ligne
d'auto-contrôle arithmétique `_(6+8+4+7+5+8+4+2+6 = 50.)_`. AJOUTER un rôle au
contrat (|rôles| 50→51) laisse titre/somme/`= 50` périmés en silence ; ÉDITER
`Ventes (8)→(7)` sans toucher la ligne casse la cohérence titre↔somme — ni les
suites tests/ (FONCTIONS RBAC, jamais la prose) ni le byte-gate d'artefact ne
l'attrapent.

Subtilité (memory rbac-spec-portail-count-gate : « §5 by-family is editorial ») :
les 9 regroupements sont ÉDITORIAUX (« Direction / Gouvernance » agrège des rôles
de familles direction+finance ≠ colonne `famille`) ⇒ AUCUN mapping groupe↔famille
(dérive fausse). On gate UNIQUEMENT ce qui est data-derived sans ce mapping.

Nouveau bloc « 1septies) §5 » : on recompute N = |rôles| du contrat
(rbac_50_roles.json, byte-gaté) et on exige (a) compte-titre == N ; (b) les 9
comptes de regroupement == EXACTEMENT dans l'ordre les termes de la ligne
(comparaison de LISTES ⇒ mord valeur ET position) ; (c) Σ termes == membre droit ;
(d) membre droit == N. Section/titre/ligne/comptes absents = échec (#6).

5 morsures vérifiées (working tree byte-restauré via git checkout -- <fichier>,
gate rc=1 sous mutation · rc=0 restauré) : titre 50→51 (a) · Ventes (8)→(9) (b) ·
= 50.→= 49. (c+d double) · terme 6+→7+ (b+c double) · suppression ligne (absence).
ci/README.md : row check-readme-claims étendue de la clause §5. 7 gates re-verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 23:46:27 +00:00
parent 15812c1dff
commit 7e674f2f09
3 changed files with 131 additions and 1 deletions
+72
View File
@@ -1377,6 +1377,78 @@ if src_roles is not None and spec_rd is not None:
good(f"RBAC SPEC §4 · (c) les {len(cm4)} entités nommées du mandat sont "
f"portées ≥1× par le contrat")
# 1septies) RBAC_50_ROLES_SPEC.md §5 — « Les N rôles (résumé) » : compte-titre,
# comptes de regroupement éditoriaux et LIGNE d'auto-contrôle arithmétique -------
# Le §5 « résumé » restitue le total de rôles sur TROIS surfaces prose qu'aucun
# gate ne touchait — (i) le TITRE « ## 5. Les N rôles (résumé) », (ii) NEUF comptes
# parenthétiques de regroupement (« **Ventes (8)** » …) et (iii) une LIGNE
# d'auto-contrôle arithmétique « _(6+8+…+6 = 50.)_ ». Le §3 gate la ventilation par
# PORTAIL, le §7 le run-book, le §4 les entités — TOUS aveugles à ce §5. NB : les 9
# regroupements sont ÉDITORIAUX (« Direction / Gouvernance » agrège des rôles de
# familles direction+finance ; ≠ la colonne `famille` du contrat, cf. memory
# rbac-spec-portail-count-gate « §5 by-family is editorial ») → on NE mappe PAS
# groupe↔famille (ce serait une dérive fausse). On gate ce qui EST data-derived et
# vérifiable SANS ce mapping : (a) le compte-titre N == |rôles| du contrat (byte-
# gaté par check_artifacts) ; (b) les 9 comptes de regroupement == EXACTEMENT, dans
# l'ordre, les termes de la ligne arithmétique (cohérence interne prose↔somme) ;
# (c) l'arithmétique est VRAIE (Σ termes == membre droit) ; (d) le membre droit ==
# |rôles|. AJOUTER un rôle au contrat (|rôles| 50→51) laisse le titre, la somme et
# le « = 50 » PÉRIMÉS en silence ; ÉDITER « Ventes (8) »→« (7) » sans toucher la
# ligne casse (b) puis (c). Section/titre/ligne/comptes absents = échec (#6).
if src_roles is not None and spec_rd is not None:
N5 = len(src_roles)
_m5 = re.search(r"## 5\..*?(?=## 6\.)", spec_rd)
if _m5 is None:
bad(f"RBAC SPEC §5 · section « ## 5. » INTROUVABLE dans {SPEC_RD} "
f"(claim absent = régression #6)")
else:
sec5 = _m5.group(0)
# (a) compte-titre « ## 5. Les N rôles »
mt5 = re.search(r"## 5\. Les (\d+) rôles", sec5)
if mt5 is None:
bad(f"RBAC SPEC §5 · titre « ## 5. Les N rôles » INTROUVABLE dans {SPEC_RD}")
elif mt5.group(1) == str(N5):
good(f"RBAC SPEC §5 · (a) compte-titre {mt5.group(1)} == |rôles| "
f"rbac_50_roles.json ({N5})")
else:
bad(f"RBAC SPEC §5 · (a) titre dit {mt5.group(1)} rôles MAIS le contrat "
f"en compte {N5} → corriger la SPEC")
# 9 comptes de regroupement (gras · « (N) » collant au **fermant)
heads = [int(m.group(1)) for m in
re.finditer(r"\*\*[^*]+?\((\d+)\)\*\*", sec5)]
# ligne d'auto-contrôle « _(a+b+… = T.)_ »
ma5 = re.search(r"_\(((?:\d+\+)+\d+) = (\d+)\.\)_", sec5)
if ma5 is None:
bad(f"RBAC SPEC §5 · ligne d'auto-contrôle « _(a+b+… = T.)_ » "
f"INTROUVABLE dans {SPEC_RD} (claim absent = régression #6)")
elif not heads:
bad(f"RBAC SPEC §5 · aucun compte de regroupement « **… (N)** » trouvé "
f"dans {SPEC_RD} (claim absent = régression #6)")
else:
addends = [int(x) for x in ma5.group(1).split("+")]
rhs = int(ma5.group(2))
# (b) comptes de regroupement == termes de la ligne, EN ORDRE
if heads == addends:
good(f"RBAC SPEC §5 · (b) {len(heads)} comptes de regroupement == "
f"termes de la ligne d'auto-contrôle ({'+'.join(map(str, addends))})")
else:
bad(f"RBAC SPEC §5 · (b) comptes de regroupement {heads} ≠ termes de "
f"la ligne {addends} (ordre/valeur) → aligner titres et somme")
# (c) arithmétique vraie
if sum(addends) == rhs:
good(f"RBAC SPEC §5 · (c) Σ termes {'+'.join(map(str, addends))} = "
f"{rhs} (arithmétique exacte)")
else:
bad(f"RBAC SPEC §5 · (c) la ligne écrit « = {rhs} » MAIS Σ termes = "
f"{sum(addends)} → corriger la somme")
# (d) membre droit == |rôles| du contrat
if rhs == N5:
good(f"RBAC SPEC §5 · (d) membre droit {rhs} == |rôles| "
f"rbac_50_roles.json ({N5})")
else:
bad(f"RBAC SPEC §5 · (d) la ligne totalise {rhs} MAIS le contrat "
f"compte {N5} rôles → SPEC périmée")
# 2) apply_plan — ligne « Génération réelle » agrégeant les 3 volets ------------
AP_RD = f"{D}/rbac/apply_plan/README.md"
try: