## The frozen planning ledgers

Three tables carry a unit from planning to delivery. Each has exactly one writer
per column set, one home, and one lifecycle. They are artifacts, not notes: a
Plan snapshot binds them, and `execute-phase` may not invent a substitute.

| Ledger | Home | Written by | Read by |
|---|---|---|---|
| Planning evidence | `### Planning evidence` inside the unit doc (XS/S) · separate file (M/L) | `unit-lane:planning-evidence` | `execute-phase`, `fold-findings`, `audit-pr` |
| Obligations | `### Obligations` inside the unit doc (XS/S) · separate file (M/L) | `unit-lane:obligations` | `execute-phase`, `fold-findings`, `audit-pr` |
| Findings | the unit doc's `## Findings` (a migrated unit, issue #308) · `review-findings.md` (a legacy unit) | reviewers append; only the stage's author resolves | `execute-phase` on re-review, `audit-pr` |

XS/S embeds both tables in the SPEC to stay within the size's artifact budget;
the Plan snapshot then binds them through the whole-SPEC row (kind `spec`), and
the `planning-evidence` / `obligations` snapshot rows are `absent`, not forged.
M/L freeze the separate files and bind them with their own rows. Never both.

**One findings carrier per unit, one read path.** A unit opened after the
`spec-by-reference` re-cut (issue #308) carries its OPEN rows in the unit doc's
`## Findings` table and its folded history, review marks (`REVIEW-RAN`), finding
marks (`VF-<n>`) and gate runs (`GATE-RAN`) as `## Log` lines whose kind token is
the `log-kind@1` vocabulary. That unit has **no `review-findings.md`**. A legacy
unit keeps the side ledger it was written with and is never rewritten to migrate
it. Readers probe the doc first and fall back to the file; a unit with the
section never reads both, so a stale side file can never outvote the doc. The
carrier change retires nothing the review machinery produces: each mark a legacy
ledger row carried is a log line here, with the same identifier, the same head
SHA and the same exit code — no new receipt enters without one retiring.

Both carriers are **one truth class**: the `review-findings` row below owns the
writers of the OPEN rows and every mark in either carrier — the map keys one *file* per
class, so a migrated unit's `## Findings`/`## Log` are that ledger inside the unit doc,
never a new class or a second owner set. One mark, two grammars: review run =
`review-mark@1` row / `## Log` `R` line; gate run = `gate-ran@1` row / `G` line; finding
verification = `finding-mark@1` row / `V` line; folded finding = the `folded: no → yes`
flip / the row leaving for an `F` line; OPEN finding = seven-column row / six-column
`findings-table@1` row. A migrated unit has no `folded` cell to flip; a legacy unit has
no `R`/`V` grammar.

### 1. Planning evidence

Row shape — the base row, the closed `authority-kind` / `freshness` vocabularies,
the prefixed stable `id`, and the `affected-decision-or-obligation` Plan-stage
extension with its full column order — is owned by the unit-lane's planning
reference: one definition, no second copy. This ledger owns only the lifecycle
rules and uses markdown headings, nothing else. Ids are stable (`PE-001`, `PE-002`, …): an
obligation or finding cites a row by id, so renumbering the table is a replan,
not a formatting edit.

Rules specific to this ledger:

- Every Engineering claim in the SPEC's Engineering half, `PLAN.md`, or a fix
  SPEC resolves to a row here. A claim with no row is not evidence-based, it is
  a guess wearing a heading.
- Raw search output, discarded hypotheses, and conversational history are
  excluded; the table is an argument index a reviewer can audit in one read.
- An assumption about a model's or service's behaviour that was never sampled is
  `unknown` with an owner — `ASSUMPTION-UNVERIFIED` is what the row *means*, and
  the row must stay visible in the artifact rather than becoming a citation.
- `execute-phase` reads only the rows whose `affected-decision-or-obligation`
names its frozen phase; the unit-lane plans the table before `execute-phase` runs.

### 2. Obligations

One row per normative behaviour, applicable compatibility invariant, affected use
case, and required failure state — created when the Engineering half is cut, and
frozen from then on except for `status`:

```text
obligation-id | authority-source | affected-use-case-or-invariant | phase |
task | implementation-owner | validator | required-evidence | status
```

| Column | Contract |
|---|---|
| `obligation-id` | stable, unique in the unit (`O1`, `O2`, … or `AC-<name>` when it mirrors a frozen criterion). Renaming an id is a replan, not an edit |
| `authority-source` | the SPEC/acceptance/criterion or ledger row that *requires* it — never the implementation |
| `phase` / `task` | exactly one phase and one task; a row needing two phases means the phase cut is wrong |
| `implementation-owner` | the skill/role that performs it (usually `execute-phase`) |
| `validator` | the command or check that proves it, copied from the acceptance artifact (`ACCEPTANCE.md` for legacy (pre-feature 61) units; `sed -n '/^## Acceptance criteria/,/^## /p' <unit>/SPEC.md \| git hash-object --stdin` for post-feature 61) or the phase's done-when. When it is a backticked reader command or one of the committed report validators (`scripts/promise-coherence.mjs` names the class), state the outcome it must produce — `` … matches <N> `` or `` … exits <N> `` — because the gate re-runs it at the head and `verified` is only truthful while it reproduces. A validator with no stated outcome is a claim nobody can check; a validator the gate declines to execute is disclosed as a skip naming why, never counted as checked |
| `required-evidence` | what must be recorded where (test name, receipt line, ledger row) |
| `status` | `planned \| in-progress \| verified \| n/a \| deferred` |

Status lifecycle and its hard edges:

- Before the unit ships, every row is `verified`. `planned`, `in-progress`,
  blank, or partial status blocks completion — as does any row deferred to a
  follow-up issue.
- `verified` requires the named validator to have run and its evidence to be
  recorded where `required-evidence` says. An assertion of intent is not a
  status change. A count or exit code in the validator cell is re-run by
  `scripts/promise-coherence.mjs`; when the command stops producing it, the row
  is stale even while it still reads `verified`.
- `n/a` needs evidence in the row (a `path:line`, ledger row, or cited contract)
  and may never contradict the SPEC's scope section. `n/a` without evidence is
  an open obligation.
- `deferred` exists only in a ledger the user has amended: deferring work out of
  the unit requires a governing-SPEC amendment first, and no route may open a
  forge issue to hold it.
- One behaviour appearing twice is a defect: merge the rows in a replan, do not
  silently drop one.

### 3. Findings

`planning-findings.md` is one stage-aware table. Reviewers append rows; nobody
edits a reviewed artifact to make a row disappear, and no reviewer mutates the
authority it approved.

```text
finding-id | stage | severity | class | snapshot-digest | claim | evidence |
status | resolution-evidence | resolving-artifact-revision
```

- `stage` is `spec` or `plan`; `severity` and `class` use the receipt
  vocabularies (`info|low|medium|high|critical`,
  `product|plan|source|environment|runtime`). Material is `medium` and above; a
  `low` row is a persisted **report-note** — visible, non-blocking, and resolved
  by the stage author without a re-review — and `info` is immaterial.
- Deflating a real defect below the `medium` minimum to unblock a pass is itself
  a finding: the row is re-classified and blocks, never waved through.
- `finding-id` is stable across cycles: a repeated finding keeps its id and gains
  a second resolution row, which is exactly what makes the no-progress and
  `CONVERGENCE-ANOMALY` rules computable.
- `status` is `open | resolved | dismissed`. `dismissed` requires
  counter-evidence that falsifies the finding ([POLICY.md](POLICY.md) §2); the
  evidence goes in `resolution-evidence`, not in chat.
- The author resolves a row through its own route: `unit-lane` for
  `class: product` and `class: plan`, and the candidate loop
  (`review-change` → `fold-findings`) only for
  `class: source | environment | runtime`.
- `resolving-artifact-revision` is the `artifactRevisionId` of the write that
  closed the row — the link that lets a re-review prove the snapshot actually
  changed.
- A `PASS` may coexist with open or unverified `low`/`info` report-note rows; it
  is refused while any open or unverified `medium`+ row exists for the bound
  snapshot. `audit-pr` reads it; the merge authority stays `audit-pr`'s alone.

## Durable ledger write ownership (the map)

The one-owner rule, stated once so every skill and template can cite it here:
**a durable ledger has exactly one writer per column set, plus at most one
declared mechanical annotator, which may append only the token its own row
names.** Nobody else writes a row — not a reviewer, not a later phase, not a
script. So `fold-findings` flips only the `folded:` flag per the map's `fold-findings:folded-flag` owner entry,
and `scripts/ledger-provenance.mjs` appends only the marker it really emits:
its `· fold <sha>` (or `· ticked <sha>` on a row that scores 1) plus the
`· REOPENED — provenance unproven` note when a tick has no matching
annotation — never a `yes` of its own.

`scripts/ledger-ownership.test.mjs` reads the block below as the single source of
truth for the seven AC16 truth classes and the projection in
`docs/fix/_TEMPLATE/LEDGERS.md` as its copy (the features-template projection
was retired at P8b). It fails closed on a missing or malformed block, an owner-less
row, drift in either direction, a token the annotator cannot produce, and any
non-test script under `scripts/` or `packages/<name>/scripts/` whose write or append
target names a durable ledger its row does not name it for. Cells carry bare values:
` · ` joins ledgers, ` + ` joins owners, an owner is `<skill>:<column-set>` (the
literal `human-owner` marks the person, the only authority for an amendment), and a
`#` line is a directive. A script that writes only generated artifacts names no
ledger and is out of scope; a copy or shell rewrite stays the reviewer's job.
Validator commands follow the repository's runtime convention: **bun first**
(`bun test …`), with the same command under `node --test …` when bun is absent —
both runtimes must pass; the CI node-compat job enforces the fallback.

```text
ledger-ownership@1
truth-class | ledger | owner | annotator | annotator-token | validator
review-findings | docs/features/<NN>-<slug>/review-findings.md · docs/fix/<issue>-<topic>/review-findings.md | review-change:finding-rows + review-change:finding-mark + review-change:review-mark + review-change:review-gate-ran-marks + audit-pr:audit-rows + triage-issue:triage-rows + execute-phase:gate-ran-marks + fold-findings:folded-flag | scripts/ledger-provenance.mjs | · fold <sha> + · ticked <sha> + · REOPENED | bun test scripts/ledger-provenance.test.mjs
planning-findings | docs/features/<NN>-<slug>/planning-findings.md · docs/fix/<issue>-<topic>/planning-findings.md | unit-lane:planning-findings + fold-findings:source-class-resolutions | none | none | bun test scripts/pre-execution-quality.test.mjs
progress | docs/features/<NN>-<slug>/SPEC.md · docs/fix/<issue>-<topic>/SPEC.md | unit-lane:progress-entries + execute-phase:phase-entries + execute-phase:gate-rejection-traces | none | none | bun test scripts/pre-execution-sensor.test.mjs
known-issues | docs/features/<NN>-<slug>/known-issues.md · docs/fix/<issue>-<topic>/known-issues.md | unit-lane:known-entries + execute-phase:blocker-entries-and-status | none | none | bun test scripts/ledger-ownership.test.mjs
decisions | docs/features/<NN>-<slug>/decisions.md · docs/fix/<issue>-<topic>/decisions.md | unit-lane:decisions + execute-phase:phase-decisions + human-owner:ratified-verdicts | none | none | bun test scripts/ledger-ownership.test.mjs
roadmap | docs/features/ROADMAP.md · docs/fix/README.md | unit-lane:row-registration + execute-phase:status-and-pr-link + audit-docs:low-risk-row-repair | none | none | bun test scripts/bounded-delivery-loops.test.mjs
acceptance-manifest | docs/features/<NN>-<slug>/ACCEPTANCE.md · docs/fix/<issue>-<topic>/ACCEPTANCE.md | unit-lane:acceptance-freeze + human-owner:approved-amendment | none | none | git hash-object docs/features/<NN>-<slug>/ACCEPTANCE.md (legacy pre-feature 61) · sed -n '/^## Acceptance criteria/,/^## /p' <unit>/SPEC.md then git hash-object --stdin (unit doc era, post-feature 61)
# no-script-writer: SPEC.md · PLAN.md · TASKS.md · CHECKLIST.md · testing.md · architecture-notes.md · planning-evidence.md · planning-obligations.md · delegated-evidence.md
```

`SPEC.md`, `PLAN.md`, `TASKS.md`, `testing.md` and `architecture-notes.md` are
durable unit records, but not ledgers with a row lifecycle of their own: their
writers are the phases that already order them (`unit-lane` creates them, `execute-phase`
ticks and appends), so the directive line keeps any script away from them instead
of forking the table above. Same for the two frozen planning ledgers at the top
of this file, and for `delegated-evidence.md`: a versioned artifact of a delegated
reading pass, whose writer and zones are stated by the triage/research step's
documentation, not by this map.

### The durable review mark

A review that ends with no fix-now finding writes no finding row, so the map gives
it a row of its own in the unit's `review-findings.md` ledger, under
`review-change:review-mark`. [POLICY.md](POLICY.md) §8 owns *when* an act marks;
this owns the shape. A **migrated** unit writes the same mark as a `## Log` `R` line —
`- **R<n>** — <date>, review at HEAD <40-hex> · verdict <verdict> · cycle <n> · gate
exit <code>` — and `scripts/workflow-status.mjs` reads either carrier, never both:

```text
review-mark@1
id | file:line | axis | severity | class | route | folded
REVIEW-RAN | HEAD <40-hex sha> | n/a | n/a | review-mark | n/a | n/a
```

`file:line` names the revision the verdict was reached against, so this ledger's
`file:line`+axis dedupe admits one mark per reviewed state, and marks append. The
other cells carry `n/a` because the row reports no finding — which keeps it out of
the fix-now projection, `fold-findings`, and the annotator (its pattern matches
`F<n>` ids only). A mark says a review ran, never that the candidate passed.

### The durable finding mark

A candidate finding that verifies — or refutes — before persistence gets a
per-finding durable signature of its own, under `review-change:finding-mark`.
This is the fix/161 extension of the feature-28 treatment: a verdict has a
signature, so must the individual finding. The single writer of every finding mark is `review-change`; nothing else appends one.

```text
finding-mark@1
id | file:line | axis | severity | class | route | folded
VF-<n> | <file:line> · reviewer review-change · HEAD <40-hex sha> · recheck <method + reproducer> | <axis> | confirmed | finding-mark | n/a | n/a
```

`VF-` ids are deliberately outside the `F<n>` range the provenance annotator's
row pattern matches, so a finding mark is invisible to the recount, to
`--check`, to `--annotate`, and to the fix-now projection. A `confirmed`
finding's row carries the mark; a `refuted` candidate carries its
counter-evidence in the same report and **never becomes a row**, a fold task,
or a re-review trigger. The mark's `confirmed | refuted` value and the
`recheck` cell (the method the finding's axis demands) are what let a driver
outside the repo consume the verification the review performed.

A **migrated** unit signs the same way as a `## Log` `V` line — `- **V<n>** —
<file:line> · reviewer <name> · HEAD <40-hex> · recheck <method + reproducer> ·
confirmed | refuted` — whose `recheck` cell `fold-findings` consumes.

`fold-findings` reads the `recheck` cell to **materialize** the finding's
reproducer as its regression check — it never re-derives it; a `recheck` cell
that cannot be materialized yields `BLOCKED <missing input>`. This consumption
leaves the row shape, the `VF-` exclusions, and the `review-change`
single-writer rule untouched.

### The gate-run mark

A gate run — green or red — leaves a durable mark in the unit's
`review-findings.md` ledger (for features:
`docs/features/<NN>-<slug>/review-findings.md`, for fixes:
`docs/fix/<issue>-<topic>/review-findings.md`), owned by the `review-findings`
truth-class row's owner cell extended with the two recorder column-sets:

```text
gate-ran@1
id | file:line | cmds | exit | [additive-slots ...]
GATE-RAN | HEAD <40-hex sha> | <cmds> | exit <code> | [reserved trailing slot: `manifest <sha>`]
```

- **Fixed format** (the one grammar; every consumer cites this block):
  `GATE-RAN | HEAD <sha> | <cmds> | exit <code>` — every run records the SHA,
  the commands run, and the exit code (green and red alike; `exit 0` is green).
  `<cmds>` is the runnable command(s) the gate ran, joined by ` | ` when several.
- **Additive slots**: fields after `HEAD` are additive; consumers ignore unknown
  trailing fields (feature 35's `manifest <sha>` slot is reserved, not yet
  consumed).
- **Identical-head reuse**: any skill may consume a **green** run only at the
  identical HEAD; a changed head ⇒ re-run the gate. A red mark is never
  consumed as a pass.
- **Recorder column-sets**: the `review-findings` truth-class row's owner cell
  declares `execute-phase:gate-ran-marks` (executor phase gates) and
  `review-change:review-gate-ran-marks` (reviewer gate runs), distinct column-set
  names per the one-writer-per-column-set rule.
- **Owner extension**: the identical owner cell is mirrored into the fix-template
  projection (`docs/fix/_TEMPLATE/LEDGERS.md`); the features-template projection
  was retired at P8b because the unit doc carries its evidence sections.
- **No new truth-class row**: the grammar admits no new row — the existing
  `review-findings.md` ledger pattern is reused with extended owner cells.
