feat(ledger): uses edges — consumption is its own relation, conformance keeps one snippet_id (#2870, milestone 294)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Failing after 39s
CI & Build / Build & push image (push) Skipped

A shape can follow one convention canon AND call several helper canons; the
row's single snippet_id made the 2026-08 audit pick (hash_token won, the
service-function convention lost), and hook evidence — pulled a snippet, then
wrote code naming it — was stamped as instance when it is a uses fact.

- code_shape_uses (migration 0084): shape → snippet, basis, evidence; unique
  per pair; cascades with both ends. USE_BASES: reference | hook | agent |
  audit | import. A judgment-grade basis overwrites a mechanical one, never
  the reverse.
- classify_shapes items and classify_shapes_by_rule take uses=[snippet ids]
  (targets validated like snippet_id; all-or-nothing).
- The write-path hook writes a uses edge for every pulled canon the payload
  names (the instance stamp is unchanged); the proposer writes a uses edge
  for every canon a body names (reference_canons: kind + language family +
  stoplist, same rules as the reference basis) — the mechanical form of
  "auto-confirm own-import references" deferred from #2871.
- list_shapes(uses=N) lists the consumers of a canon; get_snippet's consumer
  map gains `uses` beside instances/variants.

Operator decision on #2870 (2026-08-21): keep one snippet_id, add uses edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:18:25 -04:00
co-authored by Claude Fable 5
parent bfe5a461b4
commit d4c7b0e48d
8 changed files with 314 additions and 14 deletions
+18 -3
View File
@@ -44,6 +44,12 @@ async def classify_shapes(
one-off-handler, test-helper, convention-plumbing, pure-helper,
generated, script, typed-record) so the ledger can be filtered
and aggregated by kind of one-off — the prose stays the record.
uses is an OPTIONAL list of snippet ids this shape CALLS (#2870):
conformance (status + snippet_id) says what shape it is, uses
says which canonical helpers it consumes — a service function
can be an instance of the service-function convention AND use
hash_token. Consumer maps are uses edges; list_shapes(uses=N)
and get_snippet's `uses` read them.
via: Who is judging — "agent" (default), "audit" (a sweep), or
"import" (carrying maps recorded elsewhere).
@@ -69,6 +75,7 @@ async def list_shapes(
proposal: str = "",
flag: str = "",
compact: bool = False,
uses: int = 0,
) -> dict:
"""Read a project's shape ledger — `status="unclassified"` IS the todo.
@@ -84,7 +91,12 @@ async def list_shapes(
classify_shapes judgment. The human todo is `unclassified`.
path: exact file, or a directory — matches everything beneath it
(the coverage line's "largest" dirs go straight in here).
snippet_id: rows classified against this snippet — a consumer map.
snippet_id: rows classified against this snippet (instance/variant
of it — conformance).
uses: rows that CALL this snippet (#2870) — the consumer map proper,
whatever shape each row is itself; edges come from judgments
(classify_shapes uses=), the write-path hook, and the proposer's
by-name reference hits.
include_vanished: include shapes no longer in the tree (history).
limit/offset: page through big ledgers (limit caps at 500).
compact: rows as `path · symbol · kind · status · signature` plus
@@ -131,7 +143,7 @@ async def list_shapes(
uid, project_id,
status=status, path=path, snippet_id=snippet_id,
include_vanished=include_vanished, limit=limit, offset=offset,
proposal=proposal, flag=flag,
proposal=proposal, flag=flag, uses=uses,
)
return {
"shapes": [r.to_compact() if compact else r.to_dict() for r in rows],
@@ -150,6 +162,7 @@ async def classify_shapes_by_rule(
via: str = "agent",
include_judged: bool = False,
reason_code: str = "",
uses: list[int] | None = None,
) -> dict:
"""The sweep form of classify_shapes: ONE judgment applied to every
unclassified shape under a directory whose symbol matches a glob.
@@ -174,6 +187,8 @@ async def classify_shapes_by_rule(
via: "agent" (default) | "audit" | "import".
reason_code: Optional catalogue code beside the reason (see
classify_shapes) — a sweep is exactly where one applies.
uses: Optional snippet ids every matched shape CALLS (#2870) — e.g.
"every *_scheduler.py symbol uses ScheduledJob".
include_judged: By default only unjudged rows are touched —
`unclassified` and the sync's mechanical `scoped` stamp — a
sweep never silently overwrites a judgment. True re-judges every
@@ -191,7 +206,7 @@ async def classify_shapes_by_rule(
uid, project_id, path=path, status=status, pattern=pattern,
kind=kind, snippet_id=snippet_id or None, reason=reason or None,
via=via, include_judged=include_judged,
reason_code=reason_code or None,
reason_code=reason_code or None, uses=uses or None,
)
except ValueError as exc:
return {"error": str(exc)}
+4
View File
@@ -245,6 +245,10 @@ async def get_snippet(snippet_id: int) -> dict:
data["instances"] = consumers["instances"]
if consumers["variants"]:
data["variants"] = consumers["variants"]
if consumers.get("uses"):
# The call sites (#2870): shapes that use this snippet, whatever
# shape they are themselves.
data["uses"] = consumers["uses"]
return data