--- 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 or Svelte component's own styles and instance-script 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. ## The writer judges; audits check that it happened The ledger is filled from your answers, given while you still know them. When a turn wrote definitions nobody has judged, the plugin's Stop hook lists them once — with whatever the machinery holds as evidence — and you classify them in one `classify_shapes(project_id, repo=…, …)` call (the `reusing-code` skill says what each verdict means). `repo` lets a verdict land on a shape the sync has not read yet. So a project worked this way stays accounted as it grows, and an audit pass is a CHECK that the reflex held — the coverage line names what slipped — not the way rows get classified. What still arrives without you: - **The sync** stamps a snippet's own reference location `canonical` (`classified_by: mechanical`) — including a component FILE row when the snippet's location names the file and no symbol. - **The write path suggests**: when you `get_snippet` a canon and then write code that references or resembles it, the shapes being written carry it as a proposal, and the prior-art hint says "→ looks like #N". It is evidence for your verdict, never a verdict — so *pull the canon you are instantiating*; that pull is what puts it in front of you at the end of the turn. Rows the hook stamped `instance` before it stopped doing so are listed by `stamps_to_review` for judgment. - **A component is a row of its own** (`kind: "file"`, named by its stem): a file that renders and defines nothing named after itself. It is judged like any other shape. ## 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. ## Derive groups are drift, not audit material The catalogue exists so the codebase is DRY **from inception**, not as DRY as the last sweep left it. Three surfaces say so without anyone running an audit (milestone 299): - **At the write** — the prior-art hint (delivered beside a write where your client supports it; shell edits count as much as editor writes) carries a `Shape ledger at ` line when a name just written is a known **duplicate family** ("identical body in N other files, no canon"), a **repeated name** ("defined in N other files, no canon") or a **canon elsewhere** ("snippet #N at — reuse, don't redefine"). Act on it *then*: pull the canon and build from it, or derive the family now — `create_snippet` the dominant form, repoint the copies, `classify_shapes` them `instance`. A family is named once per session. - **On arrival** — the coverage line's `standing:` block (shown even when nothing is unclassified) and `derive_new` ("+N new copies since last refresh: .x in ") name what drifted since the previous refresh. That is the todo of the moment, sized to the last batch — not a backlog. - **A family that is convention, not copies** — component-local `load` / `toggle` / `save` that happen to share a name — is dismissed, not consolidated: `classify_shapes(..., status="exempt", reason_code="convention-plumbing", reason=…)` (or `classify_shapes_by_rule` for a whole family) removes it from the queue. Dismissal is a judgment and it is recorded; silence is not. - **CSS is watched by name, never by body** (note 2917). Classes serving different purposes share declarations because the style system makes them alike — `.text-muted` and `.pin-badge-auto` carrying the same `color:` are two meanings, not two copies — so a CSS family is the *same class defined in ≥2 files* (a recipe living in several places), and identical bodies under different names are never a family. Derive a CSS family by moving the recipe to the shared sheet and recording it; a class name reused for genuinely different things is dismissed with `reason_code="scoped-css"`. The datum that decides between the two is **what renders it**: every css row carries `used_by` (the files whose markup names the class — the CSS consumer map, milestone 302), a derive group carries the family's `consumers`, and the write-path line says "used by N template(s)". Many templates, one recipe → derive; one template each, different purposes → dismiss. `list_shapes(flag="unused-css")` is the map's negative space — css rules no template names, a deletion candidate to look at, never auto-deleted. The map reads the two class forms templates don't spell out — a ``'s generated classes, and the prefix of a concatenated name (`` `status-${s}` `` credits every `status-…` rule) — so what it flags is worth reading. What it still cannot see is a name assembled in a script (`classList.add`), so confirm before deleting. After the one-time pay-down the derive queue reads empty; anything in it afterwards is drift of the moment, and the hint already said so at the write. ## 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 hint 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 , 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. - **Weak stamps** — the coverage line's "N weak stamps … to judge — stamps_to_review": rows the write-path hook asserted, unattended, on a resemblance below today's floor. They no longer elect a directory's canon or raise a divergence, but they still claim to be instances. Read each one and record the verdict with `classify_shapes` — `instance` if it is, otherwise what it actually is. ## 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.