---
name: shape-accounting
description: Use when a project's shape accounting needs attention — the pattern_coverage line from enter_project shows unclassified shapes or is missing on a forge-served project, the operator asks about coverage/accounting/canon, or you just proved a code-to-canon relationship (an audit enumerated call sites, a consolidation repointed consumers, a verify pass confirmed a helper's users). Triggers on "coverage", "accounted", "unclassified", "classify shapes", "what uses this", or finishing any consolidation.
---

# Shape accounting — every shape classified against canon

The snippet library records **canon** (small); the shape ledger accounts for
**every extracted definition** in a project's bound repos (total). Each ledger
row carries a status:

- `canonical` — IS a snippet's reference (the coverage sync stamps these
  mechanically; you rarely set it).
- `instance` of snippet N — conforms to recorded canon. Canon in another
  project counts (a family-level button shape fully accounts for a local use).
- `variant` of snippet N — a deliberate, named departure. **Reason required**
  — the why IS the record.
- `exempt` — judged genuinely one-off. **Reason required.** A recorded
  judgment, not silence — it stops the next pass re-litigating it.
- `scoped` — one-off **by construction**, stamped by the coverage sync
  (a Vue component's scoped `<style>` rules and its `<script setup>`
  functions — unreachable from any other file). Accounted for without a
  judgment; still proposed against, grouped and flagged; any judgment you
  make overrides it. Not the todo.
- `unclassified` — nobody has judged it yet. **This is the todo list.**

## The loop

1. **Seed / refresh** — the ledger fills from coverage computation. Entering a
   project triggers a background seed automatically; when you need it current
   *now* (before a classification batch, or when the line is missing on a
   forge-served project), call `refresh_pattern_coverage(project_id)` — it
   returns the fresh accounting line. Takes seconds; it moves repo archives.
2. **Read the todo** — `list_shapes(project_id, status="unclassified")`,
   optionally scoped by `path` to the directories the coverage line names as
   largest. `snippet_id=N` reads a consumer map.
3. **Judge in batches** — `classify_shapes(project_id, [{path, symbol,
   status, snippet_id?, reason?}])`. All-or-nothing: a bad item applies
   nothing. Rows, never prose — a consumer list in a note or verification
   detail cannot be sorted, queried, or diffed.

## Rows that arrive on their own

Two feeds keep the ledger current between your batches, so most shapes never
need a hand judgment:

- **The sync** stamps a snippet's own reference location `canonical`
  (`classified_by: mechanical`).
- **The write path** stamps instances as you work: when you `get_snippet` a
  canon and then Write/Edit code that references or resembles it, the
  definitions being written land as `instance` rows (`classified_by: hook`,
  the evidence in `reason`), and the prior-art hook tells you what landed
  ("Shape accounting: recorded at … → instance of #N"). Offered-but-unopened
  snippets stamp nothing — so *pull the canon you are instantiating*; that
  pull is what turns your reuse into accounting. A hook row is evidence, not
  judgment: it never overrides a classification you made, and a
  `classify_shapes` call overrides it.

## The machine proposes, judgment classifies

Every coverage refresh runs the **mechanical proposer** over the unclassified
rows: same symbol as a canon elsewhere → textual containment → body
references a canon → signature resemblance → semantic (capped per refresh). A hit
is a *proposal* on the row, never a classification. Work the queue in bulk:

1. `list_shapes(project_id, proposal="canon", snippet_id=N)` or
   `path="dir"` — read the page; `proposal` carries snippet_id, basis, score.
2. `confirm_shape_proposals(project_id, snippet_id=N)` (or `path=`,
   `basis=`) for the ones that hold — hundreds at a time; `symbol` and
   `reference` proposals are near-certain, `semantic` deserves a look.
3. `classify_shapes` the rest — variant, exempt, or instance of a different
   snippet. Any judgment retires the proposal.

`list_shapes(project_id, proposal="derive")` lists the **derive-first
candidates** — the same body in ≥2 places or the same name defined in ≥3
files, with no canon at all (`proposal.group` names the family; the coverage
payload's `derive_groups` ranks the biggest). That is the consolidation
queue, not a classification queue: see below.

## The derive-first rule

N same-shaped occurrences matching **no** recorded canon is never N loose
classifications — it is a consolidation candidate: derive one reference from
the dominant form, `create_snippet` it, migrate the outliers, then classify
the rest as instances. Canon is determined from the code; consistency comes
from the derivation, not from asking permission.

## The divergence readout — button B where button A is canon

Three questions the ledger answers mechanically (#2793):

- **Divergence** — `list_shapes(project_id, flag="divergence")` (and the
  coverage line's "N DIVERGENT"): a shape new since the previous refresh, in
  a directory where one canon dominates the judged siblings, that the
  proposer did not match to that canon. `diverges_from` names the canon.
  Judge it: `instance` if it should be built from the canon (and rebuild
  it), `variant` with the why if the departure is deliberate. The write-path
  hook asks the same question in-band the moment such a shape is written.
- **History** — `shape_history(project_id, path, symbol?)`: the current rows
  plus every `classified` / `vanished` / `reappeared` / `drifted` event with
  its commit — "instance of #N from <date>, re-judged variant of #M because
  R, vanished at C". Rows, not recollection.
- **Recheck** — `list_shapes(project_id, flag="recheck")`: judged
  instances/variants whose body changed since judged. The judgment stands;
  re-confirm it (classify again with the same status) or re-judge.

## What this buys

Divergence becomes mechanical: when button B appears where button A is canon,
the ledger says *unintended divergence* or *justified variant with its
reason* — nobody re-derives the history. `get_snippet` shows each snippet's
`instances` and `variants`, so "what uses this?" is answered from rows before
any contract change lands on its consumers.
