--- 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. - `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 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. ## 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.