diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 187c206..78dbee9 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,54 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · RBAC SPEC §3 — ancrage de la colonne « Familles rattachées » PAR portail sur le contrat + +**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de +chiffres »). La table §3 « Cartographie portails ↔ familles » de +`05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md` porte DEUX colonnes data-derived +du contrat `rbac_50_roles.json` : « Nb rôles » (déjà gatée, bloc §3 count) et — +juste à sa gauche — « **Familles rattachées** » : pour chaque portail, l'ENSEMBLE +des `famille` distinctes de ses rôles (ex. `direction` → {direction, finance, +faisabilite, legal}). Cette 2ᵉ colonne était **NON gatée** : le bloc §3 count la +capture avec `[^|]*` (il ne lit QUE le compte), le §5 « résumé » traite volontairement +les regroupements comme ÉDITORIAUX (pas de mapping famille↔groupe, cf. memory +`rbac-spec-portail-count-gate`), et `check_artifacts` est aveugle (la SPEC n'est +pas un `out/*.json`). Même classe que « colonne SET gatée seulement sur son COUNT +voisin » (roleprofile per-profil count vs quels rôles). + +**Le « vert trompeur »** — reclasser la `famille` d'un rôle À L'INTÉRIEUR d'un +portail change le SET de familles **sans** changer le « Nb rôles » du portail → la +cellule §3 ment en silence pendant que le count-gate reste VERT. Prouvé (M4) : +reclasser les DEUX rôles `legal` de `direction` (Directeur Juridique + Agent ONAPI) +en `finance` laisse le compte à **9==9 VERT** tandis que le nouveau gate mord +**FANTÔME {legal}** (la SPEC liste encore `legal`, le contrat ne l'a plus). +Ajouter/retirer une famille (rôle d'une famille inédite) laisse aussi la cellule +périmée. Aucune suite `tests/` (FONCTIONS de validation, pas l'ancre SPEC↔contrat) +ne mord. + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « 1quater-bis » juste après le +§3 count-gate, RÉUTILISE `src_roles`/`spec_rd` déjà chargés) — RECOMPUTE le SET de +familles distinctes PAR portail depuis `rbac_50_roles.json[roles][].famille` et +exige, par ENSEMBLE (absent ET fantôme), l'égalité avec les jetons `famille` de la +cellule. On ne gate QUE les jetons `famille` bruts (identifiant de tête de chaque +item séparé par `,`, via `(?:^|,)\s*([a-z]+)`), **PAS** les parenthèses ÉDITORIALES +(« finance (CFO) », « faisabilite (analyste/rendu/IFC/économiste) » — paraphrase, +pas contrat, #6). Ordre NON gaté (réordonner les rôles dans le contrat byte-gaté +est sémantiquement neutre pour l'appartenance). Un portail sans cellule échoue AUSSI +(INTROUVABLE = régression de traçabilité #6). + +**Vérif** — 6 checks verts sur l'arbre propre (un par portail). **5 morsures** +adversariales : M1 SPEC retire `legal` de `direction` ⇒ MANQUE {legal} · M2 SPEC +ajoute `rh` à `achat` ⇒ FANTÔME {rh} · M3 SPEC `finance`→`financ` sur `compta` ⇒ +MANQUE {finance} · FANTÔME {financ} · **M4 SILENT-GREEN : contrat reclasse les 2 +rôles `legal`→`finance` dans `direction`, compte inchangé (count-gate 9==9 VERT) ⇒ +familles-gate mord FANTÔME {legal} — preuve que l'autorité est le CONTRAT, pas la +cohérence interne de la SPEC** · M5 SPEC supprime la ligne `direction` ⇒ cellule +INTROUVABLE. Gates re-verts : `check_artifacts` · `check_ci_integrity` · +`check_docs` · `check_readme_claims` · `check_regression` · `guard_constraints` +tous exit 0. + +--- + ## Sprint 8 · buffer · Faisabilité — ancrage du TEMPLATE CANONIQUE v1.0 (5 surfaces #9/#10) sur CLAUDE.md **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 4f0ff74..6347dc3 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -1242,6 +1242,54 @@ if src_roles is not None and spec_rd is not None: bad(f"RBAC SPEC §3 · total — {SPEC_RD} dit {mt.group(1)} MAIS " f"rbac_50_roles.json compte {exp_tot} → corriger la SPEC") +# 1quater-bis) RBAC_50_ROLES_SPEC.md §3 — colonne « Familles rattachées » PAR portail +# La MÊME table §3 porte, à GAUCHE de la colonne « Nb rôles » (gatée ci-dessus), la +# colonne « Familles rattachées » : pour chaque portail, l'ENSEMBLE des `famille` +# distinctes de ses rôles (ex. `direction` → {direction, finance, faisabilite, legal}). +# C'est un SET data-derived du contrat `rbac_50_roles.json[roles][].famille` — mais le +# bloc §3 ci-dessus l'AVALE par `[^|]*` (il ne capture QUE le compte), donc AUCUN gate +# ne le touche. Silent-green réel : reclasser la `famille` d'un rôle À L'INTÉRIEUR d'un +# portail (ex. l'unique rôle `legal` de `direction` repassé en `finance`) change le SET +# de familles SANS changer le « Nb rôles » du portail → la cellule §3 ment en silence +# pendant que le compte-gate reste VERT ; ajouter/retirer une famille d'un portail +# (nouveau rôle d'une famille inédite) laisse la cellule PÉRIMÉE. Ni check_artifacts +# (SPEC n'est pas un out/*.json) ni le §3 count-gate ni le §5 (regroupements ÉDITORIAUX, +# volontairement NON mappés famille↔groupe) ne mordent. On RECOMPUTE le SET par portail +# et on exige, par ENSEMBLE (absent ET fantôme), l'égalité avec les jetons `famille` de +# la cellule. On ne gate QUE les jetons `famille` bruts (identifiant de tête de chaque +# item séparé par `,`), PAS les parenthèses ÉDITORIALES (« finance (CFO) », « faisabilite +# (analyste) » — paraphrase, pas contrat, #6). Un portail sans cellule échoue AUSSI +# (INTROUVABLE = régression de traçabilité #6). +if src_roles is not None and spec_rd is not None: + fam_by_portail = {} + for r in src_roles: + p = r.get("portail"); f = r.get("famille") + fam_by_portail.setdefault(p, []) + if f not in fam_by_portail[p]: + fam_by_portail[p].append(f) + for portail in sorted(fam_by_portail): + exp_fam = fam_by_portail[portail] + # cellule « Familles rattachées » = 2ᵉ colonne (entre le portail et « Nb rôles ») + pat = rf"\| \*\*{re.escape(portail)}\*\*[^|]*\| ([^|]*)\| \d+ \|" + m = re.search(pat, spec_rd) + if m is None: + bad(f"RBAC SPEC §3 familles · portail `{portail}` — cellule INTROUVABLE " + f"dans {SPEC_RD} (attendu familles {{{', '.join(exp_fam)}}})") + continue + # jeton `famille` de tête de chaque item séparé par « , » (hors parenthèses éditoriales) + got_fam = re.findall(r"(?:^|,)\s*([a-z]+)", m.group(1).strip()) + absent = [f for f in exp_fam if f not in got_fam] + phantom = [f for f in got_fam if f not in exp_fam] + if not absent and not phantom: + good(f"RBAC SPEC §3 familles · portail `{portail}` — {{{', '.join(sorted(got_fam))}}} " + f"== familles distinctes du contrat") + else: + det = [] + if absent: det.append(f"MANQUE {{{', '.join(absent)}}}") + if phantom: det.append(f"FANTÔME {{{', '.join(phantom)}}}") + bad(f"RBAC SPEC §3 familles · portail `{portail}` — la cellule diverge du " + f"contrat : {' · '.join(det)} (attendu {{{', '.join(exp_fam)}}}) → corriger la SPEC") + # 1quinquies) RBAC_50_ROLES_SPEC.md §7 — comptes du run-book d'application -------- # La NOTE §7 (« ✅ … livré en-repo ») restitue, sur une SURFACE DISTINCTE des READMEs # userperm_gen / roleprofile_gen (déjà gatés), TROIS comptes data-derived du run-book