diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index 8bbda95..a6e1095 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -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. diff --git a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json index d228dde..137d66f 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json +++ b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json @@ -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", diff --git a/05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md b/05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md index 5fcc142..3e8e5a9 100644 --- a/05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md +++ b/05_deliverables_mvp/rbac/RBAC_50_ROLES_SPEC.md @@ -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 diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index a990ffc..11ec6d8 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -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: