Files
FabledGrimoire/plugin/skills/shape-accounting/SKILL.md
T
bvandeusenandClaude Opus 5.5 17777e9bb1
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 39s
docs(6204): shipped instruction text describes the product, not this instance's records
MCP tool docstrings and comments, the server's orientation notes, the plugin
skills, every hook script and the plugin README/PACKAGING drop record ids,
milestone numbers, dates and incident stories, and state each non-obvious why
as a present-tense fact. History stays in the task logs.

Corrections found on the way:
- plugin README/PACKAGING said every hook is a GET; moment, rule-opened and
  reply-rules POST, so the plugin's key needs write scope. Both now say so,
  name Settings -> MCP Access, and list every wired hook and endpoint.
- the contract-change hint printed "(lesson #4207)", this instance's record,
  to every install. It no longer cites a record.
- tasks.py claimed there is no delete_task; there is.
- rulebooks.py cited delete_event and a design spec that no longer exist.
- scribe_after_write.sh carried an always-empty etag_q; removed.
- test_write_path_trigger pinned "| tac | scribe_defs | head -1", text that
  existed only in a comment about the old code; it now pins the real tail -1
  pipeline.
- docs/development.md: CI runs on Gitea Actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:45:09 -04:00

10 KiB

name, description, metadata
name description metadata
shape-accounting 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.
moments
work.record

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:

  • 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 <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 — 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. 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), 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 <Transition name="x">'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:

  • 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 the current floor. They do not 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.