diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 632d9cb..ee7685e 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,59 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · RBAC SPEC §2 — ancrage de l'encadré « Défense en profondeur » (NOM du rôle détenteur de `set_user_permissions` · scope · famille) sur l'artefact byte-gaté + +**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention »). Le §2 +« Modèle RBAC » de `05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` ferme sur un +encadré de sécurité « **Défense en profondeur** » : le pouvoir `set_user_permissions` +(élévation de privilège) est « **réservé** au SEUL rôle `OTO Plateforme RBAC Admin` +(`scope_donnees = groupe`, `famille = plateforme`) — séparation des pouvoirs vérifiée +par test automatisé ». La SPEC se réclame donc d'un **fait de sécurité tiré du +contrat** `rbac_50_roles.json`, sur **trois attributs** : NOM du détenteur · scope · +famille. Or le test cité (`test_rbac::test_set_user_permissions_reserve_admin`) impose +bien scope==groupe ET famille==plateforme à TOUT détenteur — MAIS **jamais le NOM du +rôle** (`OTO Plateforme RBAC Admin`) NI cette prose §2. L'**UNICITÉ** (« au SEUL rôle ») +et le NOM étaient DÉJÀ gatés côté **artefact** (bloc « RBAC fixtures », singleton +`len(fx_sup)==1` depuis `custom_docperm.json` byte-gaté + prose du **README fixtures** +« émis que pour le rôle **NAME** ») — mais **PAS la 3ᵉ surface prose, le §2 de la SPEC**. + +**Le « vert trompeur »** — RENOMMER le détenteur dans le contrat (+régénérer +fixtures/out + audit 4Big) laisse le §2 citer un rôle **DISPARU** en silence : +`check_artifacts` ne voit qu'un **HASH de TAILLE** du doc (agnostique au nom · la SPEC +n'est pas un `out/*.json`), les 10 tests restent verts (scope/famille inchangés). +**Prouvé (M6 SILENT-GREEN)** : renommer `…RBAC Admin`→`…Securite Admin` dans le +contrat + régénérer les 4 générateurs RBAC & l'audit ⇒ **`check_artifacts` VERT** +pendant que le nouveau gate mord « §2 nomme ['OTO Plateforme RBAC Admin'] MAIS +set_user_permissions réservé à 'OTO Plateforme Securite Admin' » — preuve que +l'autorité est l'**ARTEFACT byte-gaté**, pas la cohérence interne de la SPEC. + +**Gate ajouté** (`ci/check_readme_claims.sh`, **extension** du bloc « RBAC fixtures », +points 4 & 5 — pas un bloc neuf) — RÉUTILISE `fx_sup` (le singleton déjà byte-gaté +ci-dessus · **zéro re-dérivation de l'unicité**, interdit #5 « jamais de doublon ») +comme **NOM-autorité** et `src_roles` (contrat byte-gaté) pour scope/famille. Capte +l'encadré `**Défense en profondeur** … vérifiée par test automatisé` (spec_rd +whitespace-normalisé) et exige : le pouvoir `set_user_permissions` y est **nommé** ; +**(a)** le NOM du détenteur cité (accents/casse neutralisés) == le seul porteur du +flag ; **(b)/(c)** les `scope_donnees = …` / `famille = …` cités == attributs du +détenteur dans `rbac_50_roles.json` ; **(5)** §6 (row « Séparation des pouvoirs ») +cite bien `set_user_permissions` réservé (anti-évaporation). Encadré/row INTROUVABLE = +échec (#6). La glose « (gestion des habilitations) » N'est PAS gatée (éditoriale). + +**Note process** (interdit #5) — un premier jet ajoutait un bloc §2 autonome +RECALCULANT le singleton depuis `src_roles` : **doublon** de la vérification d'unicité +déjà portée par le bloc fixtures (via `custom_docperm.json` byte-gaté, équivalent par +`check_artifacts`). Écarté ; les 4 checks réécrits en **extension** du bloc existant, +sans ré-affirmer l'invariant d'unicité. + +**Vérif** — 4 checks verts sur l'arbre propre. **6 morsures** adversariales, gate +lancé seul, reverts par `cp`/`tar` ciblés (jamais `git checkout` large — cf. incident +consigné) : **M1** §2 renomme le détenteur (prose) ⇒ NOM ≠ artefact · **M2** §2 +scope `groupe`→`entite` ⇒ (b) ≠ détenteur · **M3** §2 famille `plateforme`→`ventes` +⇒ (c) ≠ détenteur · **M4** encadré supprimé ⇒ INTROUVABLE · **M5** §6 retire +`set_user_permissions` ⇒ ancre évaporée · **M6 SILENT-GREEN** (ci-dessus) contrat +renommé + artefacts régénérés, `check_artifacts` VERT ⇒ §2 (a) mord. Suite complète +re-verte : `check_artifacts` · `check_docs` · `guard_constraints` · +`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. + ## Sprint 8 · buffer · Traçabilité roadmap — ancrage de la CITATION `roadmap_ref` VERBATIM (n° ligne + libellé) des artefacts sur la ligne RÉELLE de la roadmap **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention · toujours diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 25aed04..4d2a2bf 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -4516,6 +4516,72 @@ if fx_cdt is not None: (f" · en trop={extra6}" if extra6 else "") bad(f"RBAC fixtures · {SPEC_RD} §6 — énumère {sorted(got6)} MAIS " f"MANIFEST {sorted(exp6)}{detail6} → régénérer la SPEC") + # 4) RBAC_50_ROLES_SPEC.md §2 — encadré « Défense en profondeur » : 3ᵉ surface prose + # du MÊME fait de sécurité. Les points (2)/(3) ancrent la prose du README fixtures + # (« émis que pour le rôle **NAME** ») + le catalogue §6 ; le §2 « Modèle RBAC » + # ferme sur « **réservé** au seul rôle `` (`scope_donnees = groupe`, + # `famille = plateforme`) — séparation des pouvoirs vérifiée par test automatisé » : + # TROIS attributs (NOM · scope · famille) qu'AUCUN gate ne touchait. Le test + # `test_rbac::test_set_user_permissions_reserve_admin` impose scope/famille à TOUT + # détenteur mais JAMAIS le NOM du rôle ni cette prose §2 → renommer le détenteur + # (contrat + régénération fixtures/out + audit 4Big) laisse le §2 citer un rôle + # DISPARU, VERT (check_artifacts ne voit qu'un HASH de TAILLE du doc, la SPEC n'est + # pas un out/*.json). On RÉUTILISE fx_sup (singleton déjà byte-gaté ci-dessus · zéro + # re-dérivation de l'unicité #5) comme NOM-autorité et src_roles (contrat byte-gaté) + # pour scope/famille. La glose « (gestion des habilitations) » N'est PAS gatée. + if spec_rd is not None and len(fx_sup) == 1: + _su_hn = fx_sup[0] + _su_role = next((r for r in (src_roles or []) + if r.get("erpnext_role_name") == _su_hn), None) + _su_def = re.search(r"\*\*Défense en profondeur\*\*(.*?)" + r"vérifiée par test automatis", spec_rd) + if _su_def is None: + bad(f"RBAC fixtures · {SPEC_RD} §2 — encadré « **Défense en profondeur** … " + f"vérifiée par test automatisé » INTROUVABLE (anti-évaporation #6)") + else: + _su_reg = _su_def.group(1) + _su_toks = re.findall(r"`([^`]+)`", _su_reg) + # ancrage : le pouvoir lui-même doit être nommé dans l'encadré + if "set_user_permissions" not in _su_toks: + bad(f"RBAC fixtures · {SPEC_RD} §2 — encadré ne nomme pas " + f"`set_user_permissions` → mal ancré") + # (a) NOM du détenteur cité == le seul porteur du flag (byte-gaté) + if _fx_acc(_su_hn) in {_fx_acc(t) for t in _su_toks}: + good(f"RBAC fixtures · {SPEC_RD} §2 — détenteur nommé « {_su_hn} » == le " + f"seul porteur de set_user_permissions (custom_docperm.json)") + else: + _su_named = [t for t in _su_toks + if "=" not in t and t != "set_user_permissions"] + bad(f"RBAC fixtures · {SPEC_RD} §2 — nomme {_su_named} MAIS " + f"set_user_permissions réservé à {_su_hn!r} → régénérer la SPEC " + f"(rôle renommé côté contrat ?)") + # (b)/(c) scope + famille cités == attributs du détenteur (contrat byte-gaté) + if _su_role is None: + bad(f"RBAC fixtures · {SPEC_RD} §2 — détenteur {_su_hn!r} INTROUVABLE dans " + f"rbac_50_roles.json (fixtures ↔ contrat désynchronisés)") + else: + for _su_lbl in ("scope_donnees", "famille"): + _su_m = re.search(rf"{_su_lbl}\s*=\s*(\w+)", _su_reg) + _su_want = _su_role.get(_su_lbl) + if _su_m is None: + bad(f"RBAC fixtures · {SPEC_RD} §2 — `{_su_lbl} = …` INTROUVABLE " + f"dans l'encadré") + elif _su_m.group(1) == _su_want: + good(f"RBAC fixtures · {SPEC_RD} §2 — {_su_lbl} `{_su_m.group(1)}` " + f"== détenteur (rbac_50_roles.json)") + else: + bad(f"RBAC fixtures · {SPEC_RD} §2 — encadré dit {_su_lbl}=" + f"`{_su_m.group(1)}` MAIS le détenteur {_su_hn!r} a " + f"`{_su_want}` → aligner sur le contrat") + # 5) §6 row « Séparation des pouvoirs » cite le pouvoir réservé (ancre présente) + _su_m6 = re.search(r"## 6\..*?(?=## 7\.)", spec_rd) + if _su_m6 and "Séparation des pouvoirs" in _su_m6.group(0) \ + and "set_user_permissions" in _su_m6.group(0): + good(f"RBAC fixtures · {SPEC_RD} §6 — row « Séparation des pouvoirs » cite " + f"bien `set_user_permissions` réservé (ancre présente)") + else: + bad(f"RBAC fixtures · {SPEC_RD} §6 — row « Séparation des pouvoirs » / " + f"`set_user_permissions` réservé INTROUVABLE → ancre évaporée (#6)") # ============================================================================ # SEO · la COMPOSITION du graphe schema.org (JSON-LD) — un nœud racine