  - every `Kind=decision` clarification row is recorded by the lead in the approval decision ledger — one `okstra approval-decision open --ledger <approvalDecisionsPath>` call per row, before report assembly runs. Assembly reads `clarificationItems[]` from that ledger and from nowhere else, so a decision that exists only as narrative prose reaches no reader and no answer channel: `okstra user-response` cannot offer a row the ledger never carried, and the HTML prints "No further decision is needed" over the top of it. Each option is an object with eight fields:
    - `role` — `recommended` for the single best answer, `alternative` for the rest. Exactly one option per row is `recommended`.
    - `answer` — the choice itself, phrased so the user can pick it as-is. Keep it to a short phrase (roughly 120 characters); the reasoning and the consequences have their own fields below.
    - `rationale` — one sentence on why this option is on the board.
    - `reach` — exactly one of `in-repo` or `cross-repo`.
    - `scopeEffects` — optional tokens drawn from `{new-schema, deferrable}`.
    - `addedWork` — one sentence naming the work this choice creates that the other choices do not. Name the files, stages, or commands; do not substitute a cost adjective.
    - `directionChange` — one sentence naming what this choice reverses: an approved plan item, a recorded decision, an earlier answer. Name that item. When it reverses nothing, say so.
    - `disposition` — the effect of selecting the option. Use `select` for `user-decision`. Use `accept-risk` on any classification, including `correctness-critical`, when the user ends the gate and leaves the DISAGREE on the record. Use `request-revision` or `reject` when the option sends the plan back.
  - report assembly derives `approvalContext`, status, and resolution. `approvalContext` contains only `classification`, `unblockCondition`, and `recommendedDisposition`; it never copies plan or activity identifiers.
  - a `C-NNN` you name outside the row itself must be a row that exists. One place is checked: a `blocked` `endStateCoverage` row's `blockedBy.ref`, when its `kind` is `clarification` — see each phase profile's `blockedBy` rule and `validators/validate-run.py` `_validate_end_state_blocked_by`. Everywhere else — `coveredBy`, `rationale`, `verdictCard.nextStep`, `finalVerdict.nextStep`, `humanSummary.actions[]`, `recommendedNextSteps[].text` — is free prose and stays uncheckable: a shipped report legitimately writes `C-057 through C-068 are applied or carried` or `C-201 does not apply`, and a validator scanning those fields for ids would fail 15 of the 56 reports on disk. There it is on you not to send a reader after an id with no row. An id an earlier run already answered reaches this report as a carried decision — prepare seeds `carriedDecisions[]` from the run's carry-in record, and `okstra approval-decision carry` admits one that record does not answer — never by citing it bare. `crossVerification` rows are numbered `CV-NNN` so a `C-NNN` has exactly one meaning.
  - the three impact fields answer three different questions — how far the change reaches, what new work it creates, and what it overturns. Someone choosing between options needs all three, so never fold them into one sentence: whichever axis is easiest to write would silently stand in for the other two.
  - a row that omits `options[]`, offers fewer than two, or marks zero or two options as `recommended` is incomplete and must be completed before the report is finalised.
  - `expectedForm` states only the *shape* of the answer — one of the options, a file path, a number, a date. It never lists the choices again; two sources for one fact leave consumers disagreeing about which is authoritative.
  - **Enforced:** `scripts/okstra_ctl/approval_decisions.py`, `schemas/final-report-v3.0.schema.json`, and `scripts/okstra_ctl/report_assembly.py`.
