From 11c48b668ba3a9e224b0c4262bcb6c99c79b801f Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sun, 2 Aug 2026 05:10:43 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260802=5F050513]=20Sprint=208?= =?UTF-8?q?=20=C2=B7=20buffer=20=C2=B7=20RBAC=20SPEC=20=C2=A72=20:=20ancra?= =?UTF-8?q?ge=20de=20l'=C3=89NUM=C3=89RATION=20des=20scope=5Fdonnees=20(li?= =?UTF-8?q?gne=20Row-level)=20sur=20l'enum=20de=20rbac.schema.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-02.md | 43 +++++++++++++++++++++ ci/check_readme_claims.sh | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 1b3a7b8..77e0f0a 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 6b95e2f..3260821 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -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: