feat(ledger): classify_shapes + list_shapes MCP tools; get_snippet carries the consumer map (#2789, milestone 294 step 3)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Skipped

The judgment write path. classify_shapes applies a batch of classifications
to a project's live ledger rows — all-or-nothing (#2709's lesson: the whole
batch is validated, write-ACL'd, and every snippet target proven readable
before any row is touched); rows match by exact (path, symbol), kind narrows,
and shapes no live row matches come back as 'unmatched' rather than errors.
variant/exempt REQUIRE the reason — the why is the record (note 2786) — and
'unclassified' deliberately withdraws a judgment back to the todo. The 'via'
channel is caller-restricted to agent|audit|import; hook and mechanical stay
server-internal so a caller can't launder judgment as machinery.

list_shapes is the todo query (status=unclassified) with composable filters:
path is exact-or-under like recorded locations, snippet_id reads a consumer
map, include_vanished reads history; paged with the true total.

get_snippet now attaches  and  — the structured consumer
map, filtered to projects the CALLER can read so a shared snippet never side-
channels another project's file layout; attached only when non-empty (#2483).

Integration tests pin the batch atomicity, ACL gates, filter composition, the
consumer map on the MCP pull, and the SET NULL companion: a judgment whose
snippet was purged rejoins the todo on the next sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:55:47 -04:00
co-authored by Claude Fable 5
parent 9d92df2825
commit 942edd1eb5
6 changed files with 601 additions and 2 deletions
+16
View File
@@ -207,6 +207,12 @@ async def get_snippet(snippet_id: int) -> dict:
the source moved on — trust the location over the cached body and
consider verify_snippet after you look.
When the shape ledger has judgments against this snippet, the response
carries `instances` (shapes classified as conforming to it — the
structured consumer map) and/or `variants` (named departures, each with
its why). Consult them before changing the snippet's contract: they are
the call sites your change lands on (classify_shapes maintains them).
If the record belongs to someone else it carries `shared: true` with the
`owner` and your `permission`. Read that as ONE PERSON'S SUGGESTION, not as
established practice here: judge it on its merits, say whose it is when you
@@ -229,6 +235,16 @@ async def get_snippet(snippet_id: int) -> dict:
await systems_tools.attach_systems(
uid, note.user_id, data, note.id, note.project_id
)
# The structured consumer map (#2789): ledger rows judged against this
# snippet. Attached only when non-empty (#2483) — and never for projects
# the caller can't read.
from scribe.services import shape_ledger as shape_ledger_svc
consumers = await shape_ledger_svc.snippet_consumers(uid, int(note.id))
if consumers["instances"]:
data["instances"] = consumers["instances"]
if consumers["variants"]:
data["variants"] = consumers["variants"]
return data