# kinds/permission.md — one permission row, after the fact

The playbook `/ba-change` loads for `kind: permission`. The frame is in
`SKILL.md`; this file carries what is specific to a **permission row** —
one `(actor × path × portée)` line of a module's `rbac.md` human matrix.

Owner document: `<APP>/<MODULE>/rbac.md`, **human matrix only** (one row
appended or rewritten; the `ba:rbac-floor` and `ba:rbac-derived-lookups`
machine blocks are never touched — the verify compares their hash).
Grammar: `_workflow/doc-templates.md` § rbac.md; vocabulary in
`lib/permission-actions.ts` (12 actions + the `read.all` tier) and
`ba-create-rbac/SKILL.md` § Portée. No code to allocate — the row IS its
identity (`existing.exact` = the same tuple already there).

## 1. Analyse the request

One grouped AskUserQuestion:

| Question | Why it matters |
|---|---|
| **Who** — which actor of `acteur.md` (shown by label)? | RBAC-005: a row never cites an unknown actor; a new role is `/ba-change` kind=actor FIRST |
| **On what** — which section (or resource) of this module? | the node of the path, `module.section[.resource]` — no application prefix in a human row |
| **What** — which action: `access`, `read`, `create`, `update`, `delete`, `export`, `import`, `approve`, `reject`, `assign`, `execute`, `lookup`, or the `read.all` scope tier? | the closed vocabulary — anything else is not seeded |
| **On which records** — Portée `toutes` (all), `les siennes` (own), `attribuées` (assigned), `équipe` (team), `personnalisée` (custom + its filter clause)? | RBAC-007 vocabulary; `own`/`team`/`custom` need a materialised data scope |

## 2. Challenge it

- **Does the floor already give it?** `access`, `lookup`, `read`, `create`,
  `update`, `delete`, `execute` are seeded for every section/resource node by
  construction — but the floor carries NO grant: the row is still what gives
  THIS actor the right. Say it, do not skip the row.
- **Is it a permission or a rule?** « Seulement si la remise < 20 % » is a
  business rule (`/ba-change` kind=business-rule); « seulement ses propres
  dossiers » is a Portée (`les siennes`), not a rule.
- **Is it a permission or a new actor?** A row for someone who is not in
  `acteur.md` is kind=actor first.
- **Does the tuple exist?** `existing.exact` → nothing to add;
  `existing.similar` (same actor + path, other Portée) → this is an
  op=modify of that row, not a second row.
- **Can the actor reach the node?** The same actor needs `access` + `read`
  on the section (RBAC-002); write them when missing.
- **Segregation of duties** — the same actor creating AND approving, or
  submitting AND approving, the same object defeats the control (RBAC-003);
  if the user insists, the decision is theirs and is written in the row's
  justification.
- **Is it consumed by a screen action?** An `approve`/`export`/`assign` row
  is usually served by a custom action whose `permission:` must root at the
  own `module.section` grain (PRD-128).
- **Does it make a derived lookup redundant?** A `read` on a producer
  section suppresses the machine-derived `lookup` — `derive-lookup-grants
  --mode check` says so (RBAC-008).

## 3. Author

Append the row to the human matrix (actor code + label, backticked path,
Portée):

```markdown
| BA-001-AC-002 (Manager commercial) | `pipeline.opportunites.approve` | équipe |
```

- Path `module.section[.resource].action` — **no** application prefix
  (create-rbac prohibition 10: the prefix is added at the page/controller
  boundary; the machine blocks are the only app-qualified rows).
- Never duplicate the same `(actor, path)` line; a Portée change is an
  op=modify of the existing row.
- Then the verify: `expectCode` = the permission path, `target.actor` = the
  actor, `baselineCount` = `existing.count`, `machineBlocksHash` =
  `owner.machineBlocksHash`.

## 4. Verify

`report.verify.ok` true: the `(actor, path)` row is found, the row count moved
by +1 (add) or 0 (modify), no duplicate tuple, no near-miss row (a `| BA-…`
line the parser rejects — a typo in the backticks, a missing cell), and the
two machine blocks byte-identical.

## 5. Propagate — what a new row drags along

1. **Reach + duties** — `access` + `read` on the node for the same actor;
   no segregation-of-duties pair (RBAC-002 / RBAC-003).
2. **The screen action** (conditional) — the action line's `permission:` and
   the pagespec `actions[].permission` at the own `module.section` grain
   (PRD-128). Json block only in the pagespec.
3. **Derived blocks** — `derive-lookup-grants --mode check` and
   `derive-permission-floor --mode check`; on drift, the same CLI in
   `mode: derive` (RBAC-008 / RBAC-009).
4. **Seed parity** — when the module is developed, `derive-rbac-grants
   --mode check` (with `projectPath`) reports the new mapping as
   `missingGrants`: that IS the Phase 0 re-entry; seeds are additive at boot,
   nothing else to do for an ADDITION.
5. **No `/ba-create-prd`**, ever, on a module with pagespecs.

## 6. Audit

`audit-ba` with dimensions `rbac`, `cross-dimension`. What an err means here:
RBAC-005 (unknown actor), RBAC-007 (Portée outside the vocabulary), RBAC-002
(section nobody reaches), RBAC-003 (duties), RBAC-008/009 (machine blocks
stale), XD-004 (a UC actor with no permission).

## Modify variant (op=modify)

- **A narrowed Portée or a revoked grant does not reach an existing
  database through the boot seed** — it is strictly additive. The revocation
  travels only through the release-branch `derive-seed-delta` script
  (gitflow PR gate blocks a PR to main without it). Announce it; you never
  run it (red lane). `derive-rbac-grants --mode check` shows the revoked
  mapping as `extraGrants`.
- A Portée moved to `les siennes` / `équipe` / `custom` needs its
  materialisation (RBAC-006, RBAC-010).
- A changed action is a delete + add of the mapping — same delta channel for
  the removed half.
- Verify expects delta **0**.
