[DTP-Worker 20260802_050513] Sprint 8 · buffer · RBAC SPEC §2 : ancrage de l'ÉNUMÉRATION des scope_donnees (ligne Row-level) sur l'enum de rbac.schema.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-02 05:10:43 +00:00
parent bcfc0f9a9d
commit 11c48b668b
2 changed files with 116 additions and 0 deletions
+43
View File
@@ -1,5 +1,48 @@
# Activity log · 2026-08-02 · Claude Code DTP Worker
## Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'ÉNUMÉRATION des `scope_donnees` (ligne Row-level) sur l'ENUM du schéma
**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
chiffres »). La table §2 « Modèle RBAC » de
`05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` ouvre par « trois niveaux, **tous
couverts par le schéma** » puis sa 3ᵉ ligne (« **Row-level** (User Permission) |
`scope_donnees` | … ») ÉNUMÈRE à la main l'ensemble EXHAUSTIF des portées
row-level : `own` · `equipe` · `entite` (Company) · `groupe` (consolidé). Par sa
phrase d'intro, cet ensemble EST l'`enum` `scope_donnees` de `rbac.schema.json`
(la DÉFINITION du modèle). Or **aucun gate ne le touchait** : §3 compte les rôles
par portail, §4 les entités, §5 le résumé, et le bloc `userperm` gate la table
`scope → mécanisme` d'un **autre** artefact (`userperm_gen/out`) — tous aveugles à
l'énumération du §2. Même classe que §4 (colonne ancrée sur une source-de-vérité
structurée), côté schéma cette fois.
**Le « vert trompeur »** — si le schéma AJOUTE une portée (`region` pour un besoin
ops réel) ou en RETIRE une (`own` supprimé, schéma resserré), le §2 reste PÉRIMÉ en
silence : il documente un modèle qui CONTREDIT le schéma tout en se réclamant
« couvert par le schéma ». Ni `check_artifacts` (la SPEC n'est pas un `out/*.json`)
ni `tests/` (FONCTIONS de validation, jamais l'ancre SPEC↔schéma) ne mordent.
**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « 1octies », juste après le §5)
— charge l'`enum` `scope_donnees` depuis `rbac.schema.json` (source de vérité du
modèle) puis exige : **(a)** ENSEMBLE des jetons `scope` backtickés de la ligne §2
== enum (set-diff absent ET fantôme) ; **(b)** ANCRAGE — la colonne du milieu NOMME
bien `scope_donnees` (sinon on gate la mauvaise ligne) ; **(c)** sanity — toute
portée RÉELLEMENT exercée par le contrat ∈ enum (une portée en usage jamais dans
l'enum = schéma/contrat incohérents). Les gloses parenthétiques `(Company)`/
`(consolidé)` NE sont PAS gatées (éditoriales · #6). Ligne/section/enum absents =
échec (traçabilité #6).
**Vérif** — 3 checks verts sur l'arbre propre (4 portées `own/equipe/entite/groupe`
== enum). **6 morsures** adversariales : M1 §2 retire `own` ⇒ manquant (a) · M2 §2
ajoute fantôme `region` ⇒ fantôme (a) · **M3 SILENT-GREEN : le schéma GAGNE `region`,
§2 inchangé ⇒ (a) mord `manquant region` — preuve que l'autorité est le SCHÉMA, pas
la cohérence interne de la SPEC** · M4 colonne du milieu renommée `scope_donnees`
`scope` ⇒ ancre perdue (b) · M5 ligne Row-level supprimée ⇒ INTROUVABLE · M6 le
schéma RETIRE `own` (encore exercé par 2 rôles) ⇒ (a) fantôme + (c) portée hors
enum. Gates re-verts : `check_artifacts` · `check_docs` · `guard_constraints` ·
`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0.
---
## Sprint 8 · buffer · Faisabilité — ancrage du DOMAINE DES CODES PROJET (garde d'entrée `_validate_brief` + 2 schémas JSON) sur CLAUDE.md §Projets
**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de
+73
View File
@@ -1497,6 +1497,79 @@ if src_roles is not None and spec_rd is not None:
bad(f"RBAC SPEC §5 · (d) la ligne totalise {rhs} MAIS le contrat "
f"compte {N5} rôles → SPEC périmée")
# 1octies) RBAC_50_ROLES_SPEC.md §2 — ligne « Row-level » : ENSEMBLE des `scope_donnees`
# ancré sur l'ENUM du schéma ---------------------------------------------------
# Le §2 « Modèle RBAC » ouvre par « trois niveaux, tous couverts PAR LE SCHÉMA » puis
# sa 3ᵉ ligne de table (« **Row-level** (User Permission) | `scope_donnees` | … ») ÉNUMÈRE
# À LA MAIN l'ensemble EXHAUSTIF des portées row-level : `own` · `equipe` · `entite`
# (Company) · `groupe` (consolidé). Cet ensemble EST, par la phrase d'intro, l'`enum`
# `scope_donnees` de `rbac.schema.json` (la DÉFINITION du modèle — pas le sous-ensemble
# effectivement exercé par les 50 rôles, qui peut être plus petit). AUCUN gate ne le
# touchait : §3 compte les rôles par portail, §4 les entités, §5 le résumé, le bloc
# userperm gate la TABLE `scope → mécanisme` d'un AUTRE artefact (userperm_gen/out) —
# tous AVEUGLES à l'énumération du §2. « Vert trompeur » : le schéma AJOUTE une portée
# (`region` pour un besoin ops réel) ou en RETIRE une (`own` supprimé, schéma resserré)
# et le §2 reste PÉRIMÉ en silence — il documente un modèle qui contredit le schéma
# tout en se réclamant « couvert par le schéma ». Ni check_artifacts (la SPEC n'est pas
# un out/*.json) ni tests/ (FONCTIONS de validation, jamais l'ancre SPEC↔schéma) ne
# mordent. Même classe que §4 (colonne ancrée sur une source-de-vérité structurée).
# GATE : (a) ENSEMBLE des jetons `scope` de la ligne §2 == `enum` scope_donnees du
# schéma (set-diff absent ET fantôme) ; (b) ANCRAGE — la colonne du milieu NOMME bien
# `scope_donnees` (sinon on gate la mauvaise ligne) ; (c) sanity — toute portée
# RÉELLEMENT exercée par le contrat ∈ enum (une portée en usage jamais dans l'enum =
# schéma incohérent). On NE gate PAS les gloses parenthétiques `(Company)`/`(consolidé)`
# (éditoriales · #6). Ligne/section/enum absents = échec (traçabilité #6).
if src_roles is not None and spec_rd is not None:
try:
_sch = load("rbac/rbac.schema.json")
_enum = _sch["definitions"]["role"]["properties"]["scope_donnees"]["enum"]
scope_enum = set(_enum)
if not scope_enum:
raise ValueError("enum scope_donnees vide")
if len(scope_enum) != len(_enum):
raise ValueError(f"enum scope_donnees non unique : {_enum}")
except (OSError, KeyError, ValueError, TypeError) as e:
bad(f"RBAC SPEC §2 · rbac.schema.json enum scope_donnees illisible : {e}")
scope_enum = None
_m2 = re.search(r"## 2\..*?(?=## 3\.)", spec_rd)
if _m2 is None:
bad(f"RBAC SPEC §2 · section « ## 2. » INTROUVABLE dans {SPEC_RD} "
f"(claim absent = régression #6)")
elif scope_enum is not None:
# Ligne « Row-level » : capte la colonne « champ » (c2) et la colonne énum (c3)
_row = re.search(r"\| \*\*Row-level\*\*[^|]*\|([^|]*)\|([^|]*)\|", _m2.group(0))
if _row is None:
bad(f"RBAC SPEC §2 · ligne « **Row-level** » INTROUVABLE dans {SPEC_RD} "
f"(claim absent = régression #6)")
else:
c_field, c_enum = _row.group(1), _row.group(2)
# (b) la colonne du milieu doit nommer le champ `scope_donnees`
if "scope_donnees" not in c_field:
bad(f"RBAC SPEC §2 · (b) colonne « champ » de la ligne Row-level ne "
f"nomme pas `scope_donnees` (capté {c_field!r}) → ligne mal ancrée")
else:
good("RBAC SPEC §2 · (b) ligne Row-level ancrée sur le champ "
"`scope_donnees`")
spec_scopes = set(re.findall(r"`([a-z_]+)`", c_enum))
_miss = sorted(scope_enum - spec_scopes) # dans l'enum, absents du §2
_phan = sorted(spec_scopes - scope_enum) # listés au §2, hors enum
if _miss or _phan:
bad(f"RBAC SPEC §2 · (a) énumération `scope_donnees` ≠ enum du "
f"schéma — manquants={_miss} · fantômes={_phan} → aligner la "
f"SPEC sur rbac.schema.json (ou régénérer si le schéma a changé)")
else:
good(f"RBAC SPEC §2 · (a) {len(spec_scopes)} portées row-level "
f"{sorted(spec_scopes)} == enum scope_donnees du schéma")
# (c) sanity : toute portée exercée par le contrat ∈ enum
_used = {r.get("scope_donnees") for r in src_roles}
_illegal = sorted(_used - scope_enum)
if _illegal:
bad(f"RBAC SPEC §2 · (c) portées `scope_donnees` exercées par le contrat "
f"HORS enum du schéma : {_illegal} → schéma/contrat incohérents")
else:
good(f"RBAC SPEC §2 · (c) les {len(_used)} portées exercées par le contrat "
f"sont toutes dans l'enum du schéma")
# 2) apply_plan — ligne « Génération réelle » agrégeant les 3 volets ------------
AP_RD = f"{D}/rbac/apply_plan/README.md"
try: