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
+29
View File
@@ -391,6 +391,35 @@ def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
assert noisy not in compact
def test_uses_edges_table_and_validation():
"""#2870: consumption is its own relation — a table that cascades with
both ends, and `uses` on a classification must be a list of ids."""
from scribe.models import Base
from scribe.models.code_shape import USE_BASES, CodeShapeUse
from scribe.services.shape_ledger import validate_classifications
assert "code_shape_uses" in Base.metadata.tables
cols = CodeShapeUse.__table__.c
assert next(iter(cols.shape_id.foreign_keys)).ondelete == "CASCADE"
assert next(iter(cols.snippet_id.foreign_keys)).ondelete == "CASCADE"
assert set(USE_BASES) == {"reference", "hook", "agent", "audit", "import"}
ok = [{"path": "a.py", "symbol": "f", "status": "instance", "snippet_id": 9, "uses": [3, 4]}]
assert validate_classifications(ok) is None
bad = [{"path": "a.py", "symbol": "f", "status": "instance", "snippet_id": 9, "uses": "3"}]
assert "uses must be a list" in validate_classifications(bad)
def test_reference_canons_names_every_used_canon_not_just_the_best():
from scribe.services.shape_ledger import Canon, _norm_text, reference_canons
a = Canon(1, "sym", "hash_token", (("src/x.py", "hash_token"),), "def hash_token(raw):", _norm_text("x"), 2, "python")
b = Canon(2, "sym", "rules_payload", (("src/y.py", "rules_payload"),), "def rules_payload(r):", _norm_text("y"), 2, "python")
ts = Canon(3, "sym", "fmtDate", (("f/d.ts", "fmtDate"),), "export function fmtDate(iso: string): string {", _norm_text("z"), 2, "typescript")
body = "def create_invitation(email):\n h = hash_token(raw)\n return rules_payload(h)\n"
assert reference_canons("sym", "src/scribe/services/auth.py", "create_invitation", body, [a, b, ts]) == [1, 2]
# the shape's own name and the other language family are never "uses"
assert reference_canons("sym", "src/x.py", "hash_token", body, [a]) == []
assert reference_canons("sym", "f/v.vue", "show", "fmtDate(x); hash_token(y)", [a, ts]) == [3]
def test_reason_codes_are_a_fixed_catalogue_and_validated():
"""#2874: an optional index beside the prose reason; unknown codes are a
structural error (the batch applies nothing)."""