CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
151 lines
8.6 KiB
Markdown
151 lines
8.6 KiB
Markdown
---
|
|
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.
|
|
|
|
## 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 (the Write/Edit hook, and since
|
|
0.1.39 the after-write hook on Bash, so sed/heredoc/script edits count too)
|
|
carries a `Shape ledger at <path>` 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 <path> — 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 <path>") 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"`.
|
|
|
|
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
|
|
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.
|