[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
+40
View File
@@ -1011,3 +1011,43 @@ absolu) · **7 gates re-verts** (`check_readme_claims`/`check_ci_integrity`/`che
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'obtention
réelle des 7 confirmations auprès de leurs owners reste côté agent DevOps / VPS).
- **Auto-score 4Big** : 96/100.
## Session `20260801_093301` · Buffer S8 · RBAC/RBAC_50_ROLES_SPEC §3 — DÉFAUT FACTUEL corrigé + gate d'identité par portail
**Contexte** : Sprint 8 · buffer (roadmap fonctionnellement close ; poursuite de la
série anti-dérive CLAUDE.md #6). Cette fois la surface data-derived non gatée était
**FAUSSE**, pas seulement à risque : la table §3 « Cartographie portails ↔ familles »
de `RBAC_50_ROLES_SPEC.md` (colonne « Nb rôles » PAR portail) avait dérivé du contrat
`rbac_50_roles.json` **en préservant le total 50**, donc en silence.
**Défaut factuel trouvé & corrigé** :
- `05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md:47` — portail `direction` affichait
**10** rôles ; le contrat en compte **9** (6 direction + CFO/finance + analyste
faisabilité + 2 legal).
- `…:52` — portail `plateforme` affichait **5** ; le contrat en compte **6** (devops ·
rbac-admin · qa · otoia · mobile · bi).
- L'erreur était compensée (10+5 vs 9+6) → total 50 exact → invisible aux gates
agrégés. Le MANIFEST roleprofile (byte-gaté) disait déjà 9 et 6 : la SPEC seule
avait dérivé. Corrigé pour refléter la source.
**Dérive silencieuse fermée (gate durable)** :
- `ci/check_readme_claims.sh` — nouveau bloc « 1quater) RBAC_50_ROLES_SPEC §3 » : RECOMPTE
le nombre de rôles PAR portail depuis `rbac/rbac_50_roles.json` (source de vérité,
byte-gatée par `check_artifacts`) et exige l'égalité avec CHAQUE ligne de la table §3
**plus le total** `**Total** | **50**`. Un portail absent de la table échoue aussi
(disparition = régression de traçabilité). Complète le gate roleprofile (qui gate le
MANIFEST, un doc distinct) sur une 2ᵉ surface transcrivant la même ventilation.
- Vérif négative : réintroduire `direction=10` fait échouer le gate (exit 1, message
« dit 10 MAIS rbac_50_roles.json compte 9 ») ; restauré → vert.
**Artefact régénéré (reproductibilité #artifact-reproducibility-gate)** :
- `qa/audit_4big/out/quality_report.json` — l'`evidence` DOC cite la taille en octets de
la SPEC (10040 → 10039 octets après correction 1 o) ; rebuild `audit_4big_gen.py build
-o out` byte-identique. `check_artifacts` re-vert.
**CI** : les 7 gates PASS (validate_json · check_artifacts · check_docs ·
check_readme_claims · guard_constraints · check_regression · check_ci_integrity).
- **Hors périmètre worker (VPS · #8)** : néant (correction d'un doc en-repo + gate
bash/python3 stdlib ; l'unique artefact `out/` touché a été régénéré à l'identique).
- **Auto-score 4Big** : 96/100.
@@ -120,7 +120,7 @@
"criterion": "DOC",
"weight": 20,
"passed": true,
"evidence": "RBAC_50_ROLES_SPEC.md (10040 octets)"
"evidence": "RBAC_50_ROLES_SPEC.md (10039 octets)"
},
{
"criterion": "CONTRAT",
@@ -44,12 +44,12 @@ technique transversale** (`plateforme`, hors du compte de 5) :
| Portail | Familles rattachées | Nb rôles |
|---|---|---|
| **direction** | direction, finance (CFO), faisabilite (analyste), legal | 10 |
| **direction** | direction, finance (CFO), faisabilite (analyste), legal | 9 |
| **ventes** | ventes, marketing | 12 |
| **construction** | construction, faisabilite (rendu/IFC/économiste) | 10 |
| **achat** | achat | 5 |
| **compta** | finance | 8 |
| **plateforme** _(technique)_ | plateforme | 5 |
| **plateforme** _(technique)_ | plateforme | 6 |
| | **Total** | **50** |
> Répartition indicative issue de `rbac_50_roles.json` ; la source de vérité reste
+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: