---
name: ba-audit-rbac
description: >
  Audits the RBAC permission matrix of a `.smartstack/ba/` module — minimum
  permissions, segregation of duties, actor coverage, reference integrity,
  data-scope coherence, derived-lookup freshness, permission-floor mirror
  freshness, and unmaterialized `team`/`custom` Portées said out loud
  (RBAC-001..010). Reads the module `rbac.md`, the app `acteur.md`
  and the section `use-case.md`; RBAC-008/009 reuse the `derive-lookup-grants`
  and `derive-permission-floor` engines by direct import (their shared source
  factories + drift layer, never a spawn), writes a verdict to
  `_audit/rbac.md`. Run after `/ba-create-rbac` or as part of pre-dev readiness.
allowed-tools: [Read, Write, Glob, Grep, Bash]  # Bash: the audit-ba engine run
---

# ba-audit-rbac — RBAC permissions audit

You audit the RBAC permission matrix of a `.smartstack/ba/` module against the
rules below and write a verdict file. The rules are unchanged from the SmartStack
convention; only the I/O is file-based.

## Deterministic engine — how this audit runs

The MECHANICAL rules of this dimension are evaluated by the shared `audit-ba`
CLI (see `/ba-audit-run`) — **never by reading the corpus yourself, never by
spawning per-module subagents** (the 394M-token incident shape). Your only
job here is the judgment residue.

1. **Run the engine, scoped to this dimension**:

   ```bash
   npx --prefer-offline tsx skills/business-analyse/audit-run/cli/audit-ba/index.ts \
     --spec '{"baRoot":".smartstack/ba","scope":{"app":"<APP>","module":"<MODULE>"},"dimensions":["rbac"]}'
   ```

2. **Exit 3 = parsing suspect -> STOP.** A control counter disagrees with the
   parser (`report.parseControl.perDoc`): fix the doc's form or report the
   parser bug, then re-run. Never « complete by hand » — no green verdict may
   be born from a silent parser.
3. **No judgment rules in this dimension** — a single engine run suffices; the verdict is final.
4. **Chat summary** (3-6 lines, business terms): the PARSE TOTALS (say the
   counts — that is how a « 0 erreur » stays verifiable), err/warn counts,
   remaining judgments, and the fix skill each finding names.

The CLI writes the verdict to `.smartstack/ba/<APP>/<MODULE>/_audit/rbac.md`
(existing format — anchor, `Verdict :` header, emoji sections; `0 err` =
pass for the downstream gate). The rule texts below remain the AUTHORITATIVE
spec — the CLI registry is drift-tested against them.

## Scope

- **Module scope** (default): audit one module — apply RBAC-001..010 to its
  matrix. RBAC-004 only checks actors that have at least one permission entry in
  this application's modules; do NOT flag actors from other applications as
  missing permissions in this module. RBAC-001..007 read the HUMAN matrix only
  (ignore BOTH machine-owned blocks — `ba:rbac-derived-lookups` belongs to
  RBAC-008, `ba:rbac-floor` to RBAC-009).
- The verdict file lives at the audited module's `_audit/rbac.md`.
- **The floor is not a grant**: the `ba:rbac-floor` block lists the permission
  ROWS that exist by construction (seeded without mappings) — never count its
  lines as actor coverage, and never flag a human row as "duplicate of the
  floor" (a human row that repeats a floor path is a GRANT of that path).

## Rules

### RBAC-001 — At least 1 permission exists per module
- **Severity**: err (if 0), ok (if >= 1)
- **ok**: the module's matrix has at least one permission entry (state the count).
- **err**: the module has zero permissions — name the module.
- Fix: `/ba-create-rbac`.

### RBAC-002 — Every section has minimum permissions (access + read) for at least 1 actor
- **Severity**: err (if missing), ok (if all have)
- Check: for each section, at least 1 actor must have both `access` and `read` actions.
- **err**: list the sections lacking the minimum (access + read) for any actor.
- Fix: `/ba-create-rbac`.

### RBAC-003 — No segregation of duties violations
- **Severity**: warn (if found), ok (if none)
- **Toxic pairs** (same actor, same target must NOT have both):
  - `create` + `approve` (creator should not approve their own work)
  - `delete` + `restore` (prevents abuse)
- **warn**: list the offending actor × target × pair violations.
- Fix: `/ba-create-rbac`.

### RBAC-004 — All defined actors have at least 1 permission in the module
- **Severity**: warn (if orphan actors), ok (if all have)
- **warn**: list the actors (of this application) with no permission entry in the module.
- Fix: `/ba-create-rbac`.

### RBAC-005 — All permission actor references point to existing actors
- **Severity**: err (if unknown actor IDs), ok (if all valid)
- Check: every `BA-…-AC-…` referenced in the matrix is authored in the app's `acteur.md`.
- **err**: list the unknown actor IDs.
- Fix: `/ba-create-rbac`.

### RBAC-006 — Scope coherence (data-scope materialization)
- **Severity**: err (if incoherent), ok (if coherent or module not scoped)
- A module is **scoped** when at least one actor's `read` has a Portée other
  than `all`/`toutes` (i.e. `own`/`assigned`). In a scoped module:
  - every actor whose `read` Portée is `all`/`toutes` MUST also have the
    matching `… .read.all` line (that permission is what opens the global
    row-level view at runtime — see the materialization rule in
    `/ba-create-rbac`);
  - an actor whose `read` Portée is `own`/`assigned` must NOT hold
    `… .read.all` (it would silently void their scope).
- In a NON-scoped module, no `… .read.all` line should exist.
- `team`/`équipe` and `custom` scopes are DESCRIPTIVE (no runtime
  materialization yet — team lands with the HR application): they are outside
  the `.read.all` pairing rule and must never be "fixed" by granting `.read.all`
  or a producer `read`.
- **err**: list the offending actor × permission rows and which side of the rule
  they violate.
- Fix: `/ba-create-rbac`.

### RBAC-007 — Scope vocabulary is closed
- **Severity**: err (if unknown scope), ok (if all valid)
- The Portée column only accepts: `all`/`toutes`, `own`/`les siennes`,
  `assigned`/`attribuées`, `team`/`équipe`, `custom`/`personnalisée` (a `custom`
  cell must carry its filter clause). Empty cells default to `all`.
- **err**: list the rows whose Portée is outside the vocabulary (typos like
  `mine`, `self`, `assignee` must be folded into the canonical values).
- Fix: `/ba-create-rbac`.

### RBAC-008 — Derived lookup grants are fresh (FK coverage)
- **Severity**: err (drift), warn (unresolved producers), ok (block up to date)
- Deterministic check — the `audit-ba` engine runs it for you, calling the
  create-rbac derivation by DIRECT IMPORT through two shared seams:
  `fsLookupSources(baRoot, consumerRows, graph?)` builds the sources,
  `checkLookupDrift(content, core)` renders the drift — the SAME code the CLI
  runs in `"mode":"check"`.
- **Never rebuild those sources by hand.** Reconstructing them from the audit
  corpus is what made this rule unclosable: `loadCorpus` filters modules by
  scope, so under a module-scoped run the producer matrices came back empty,
  the engine's `holds-read` skip could not fire, and the rule demanded grants
  the writer had rightly skipped (client report, 2026-09-04). Whatever the
  scoped corpus cannot supply full-tree must come from the factory.
- To reproduce the same verdict outside the engine (support, debugging) — its
  `report.drift` equals the rule's, by construction and by test:

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-rbac/cli/derive-lookup-grants/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"check"}'
  ```

- Mapping from `report.drift`:
  - `missingBlock: true` (while `totals.fkCandidates > 0`) or non-empty
    `staleRows`/`missingRows` → **err** — the module's create/update actors
    would hit 403 on their FK dropdowns, or keep stale grants. Name the rows.
  - `report.needsResolution` entries → **warn** — producer section unresolved
    (no list screen, no matching menu section). Name the FK and the producer.
  - `drift.upToDate: true` and no `needsResolution` → **ok**.
- The CLI exits 0 even on drift — drift is DATA; THIS rule is the enforcement.
- If the CLI fails because `entité.md` is absent/placeholder (phase 6 not run
  yet), mark RBAC-008 **ok** with the note « data model not yet authored —
  check deferred to the post-data-model run ».
- Fix: re-run the CLI in `"mode":"derive"` (NEVER hand-edit the machine block);
  for warns, complete the producer's list screen or menu section, then re-run.
- **A skipped grant is not a missing one.** An actor already holding the
  producer section's `read`/`lookup` gets NO derived row — the dual gate
  `[RequirePermission(x.lookup, x.read)]` (ANY) already lets them through, and
  the Portée of that held `read` does not change it (see `/ba-create-rbac`).
  The machine block lists those suppressions under the table, so an empty block
  says WHICH of the two reasons it is.

### RBAC-009 — The permission-floor mirror block is fresh (menu coverage)
- **Severity**: err (drift), warn (reserved node codes), ok (block up to date)
- Deterministic check — run the colocated create-rbac CLI (Bash) and read its
  JSON envelope; never re-derive by hand:

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-rbac/cli/derive-permission-floor/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"check"}'
  ```

- Mapping from the envelope's `report.drift`:
  - `missingBlock: true` or non-empty `staleRows`/`missingRows` → **err** —
    the MENU moved after the mirror was written: the human review reads a
    floor that no longer matches what the seed will create, and human rows may
    reference dead nodes. Name the stale/missing node rows.
  - `report.warnings` entries (reserved node codes `read`/`all`) → **warn** —
    those codes break the `….read.all` scope-tier disambiguation of permission
    paths; rename the section/resource in the menu.
  - `drift.upToDate: true` and no warnings → **ok**.
- The CLI exits 0 even on drift — drift is DATA; THIS rule is the enforcement.
- A floor drift never breaks the SEED itself (scaffold-core-seed derives the
  floor from the nav tree directly) — it breaks the human review mirror and
  signals a menu change the rest of the matrix may not have followed.
- Fix: re-run the CLI in `"mode":"derive"` (NEVER hand-edit the machine block).

### RBAC-010 — `team`/`custom` Portées are declared expectations, not runtime restrictions
- **Severity**: warn (any row whose Portée is `team`/`équipe` or
  `custom`/`personnalisée`), ok (none)
- Neither scope has ANY runtime materialization today (create-rbac § Condition
  scopes: `team` awaits the HR-backed teams source; `custom` means a
  hand-written LINQ filter someone must still write). At runtime the actor
  therefore reads **ALL rows** — the BA believes it modelled a restriction,
  the deployed app has none. The generators emit nothing for these scopes and
  no dev-side audit flags them, so THIS warn is the only place the gap is ever
  said out loud.
- **warn**: list each `(actor, permission)` row concerned, state explicitly
  « équivaut à `toutes` au runtime tant que la portée n'est pas matérialisée »,
  and name the follow-up: keep the row as a documented intention, plan the
  hand-written filter (`custom`), or wait for the HR teams source (`team`) —
  never silently downgrade the cell to `toutes`, the intention is data.
- Fix: none required (the warn IS the deliverable) — or re-scope the row to
  `own`/`assigned`/`toutes` if the restriction was not actually needed.

## Output

Write `_audit/rbac.md` per the doc-templates skeleton:
- Header `# Audit rbac — <APP> / <MODULE>` + `_<date> · Verdict : <emoji> N warn · M err · K ok_`.
- `## ✅ Conforme`, `## ⚠️ Avertissements`, `## ❌ Bloquants` sections; one bullet
  per finding. For `warn`/`err`: what's wrong (offending actor/permission/section
  codes **bold**), why it matters, and a `→` fix naming `/ba-create-rbac`. Include
  the concrete offending permission/actor/section names so the fix is actionable.
- Re-Write the whole file each run (overwrite — it's a fresh verdict).

Then a 3–6 line chat summary in the user's language — business terms, not rule
codes. If any `err`, state clearly that the permissions must be fixed before moving on.

## Used by the readiness orchestrator

`/ba-audit-pre-dev` runs every dimension and aggregates the verdicts. When invoked
by it, still write `_audit/rbac.md` as usual — the orchestrator reads these files.
