feat(ledger): mechanical proposer — every refresh proposes instances against canon and groups derive-first candidates; agents confirm in batches (#2792, milestone 294 step 6)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 48s

Shapes now carry a content fingerprint (signature + whitespace/comment-
insensitive body_sha; migration 0080) and the proposer runs inside the
coverage refresh, the one moment bodies exist: symbol elsewhere → textual
containment → body references the canon → signature resemblance → semantic
(capped per refresh, unreached rows stay unexamined for the next). A hit is
a proposal on the row (proposed_snippet_id/basis/score), never a
classification; rows with no canon hit group by the derive-first rule
(identical body in ≥2 places, same name in ≥3 files) as proposal_basis=
derive + a group key. list_shapes(proposal=any|canon|derive|<basis>) is the
queue; confirm_shape_proposals(project_id, snippet_id|path|basis) confirms
in batches as agent instances; any classify_shapes/hook stamp retires the
proposal. Readout carries proposed + derive_groups (line, payload, card).
Plugin 0.1.35 (skill: the machine proposes, judgment classifies).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 21:30:35 -04:00
co-authored by Claude Fable 5
parent a9e1cddba7
commit ba0030e51d
12 changed files with 1190 additions and 43 deletions
+142
View File
@@ -351,3 +351,145 @@ async def test_recent_pulls_reads_the_usage_stream(seeded):
assert sid in pulls
assert sid + 1000 not in pulls
assert sid + 2000 not in pulls
# --- #2792: the mechanical proposer against real rows -------------------------
def _quiet_semantic():
"""The semantic basis needs the embedder; these tests prove the other
bases and the bookkeeping, so it answers "nothing" here."""
from unittest.mock import AsyncMock, patch
from scribe.services import shape_ledger
return patch.object(shape_ledger, "_semantic_canon", AsyncMock(return_value=None))
def _defs(*items):
"""ArchiveShape-like records: (path, kind, name, signature, body_sha, body)."""
import hashlib
out = []
for path, kind, name, signature, body in items:
sha = hashlib.sha1(" ".join(body.split()).encode()).hexdigest()[:16]
out.append((path, kind, name, signature, sha, body))
return out
@pytest.mark.integration
async def test_proposer_proposes_and_confirm_classifies(seeded):
"""Bodies in hand, the proposer records proposals on unclassified rows —
symbol (a second `factory` elsewhere), reference (a call site), and
nothing for the unrelated — skips rows whose content it already judged,
and a scoped confirm turns proposals into agent instances while a
classify on another retires its proposal."""
from scribe.services.shape_ledger import (
confirm_proposals, propose_for_repo,
)
owner, other, pid, sid = (
seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
)
defs = _defs(
("src/app.py", "sym", "make_app", "def make_app():", "def make_app():\n app = factory()\n return app"),
("src/app.py", "sym", "Config", "class Config:", "class Config:\n debug = False"),
("src/util.py", "sym", "helper", "def helper(x):", "def helper(x):\n return x"),
("src/dup.py", "sym", "factory", "def factory():", "def factory():\n return 1"),
("web/button.css", "css", "btn", ".btn {", ".btn {\n color: red;\n}"),
)
await sync_repo_shapes(pid, REPO, defs, seen_marker="main")
with _quiet_semantic():
stats = await propose_for_repo(owner, pid, REPO, defs)
assert stats == {"examined": 5, "proposed": 2, "semantic_checked": 2}
rows, total = await list_project_shapes(owner, pid, proposal="canon")
by_symbol = {r.symbol: r for r in rows}
assert total == 2
assert by_symbol["factory"].proposal == {"basis": "symbol", "score": 1.0, "snippet_id": sid}
assert by_symbol["make_app"].proposal == {"basis": "reference", "score": 0.9, "snippet_id": sid}
rows, _ = await list_project_shapes(owner, pid, proposal="reference")
assert [r.symbol for r in rows] == ["make_app"]
# Same content again → nothing re-examined (the semantic cap would
# otherwise be spent on the same rows every refresh). A cap that leaves
# rows unreached leaves them UNexamined, so the next refresh gets them.
with _quiet_semantic():
assert (await propose_for_repo(owner, pid, REPO, defs))["examined"] == 0
await classify_shapes(owner, pid, [
{"path": "src/util.py", "symbol": "helper", "status": "unclassified"},
])
assert (await propose_for_repo(owner, pid, REPO, defs, semantic_cap=0))["semantic_checked"] == 0
assert (await propose_for_repo(owner, pid, REPO, defs))["examined"] == 1
# Outsider can't confirm; the owner confirms by snippet, scoped.
with pytest.raises(ValueError):
await confirm_proposals(other, pid, snippet_id=sid)
assert await confirm_proposals(owner, pid, basis="symbol") == {"confirmed": 1}
rows, _ = await list_project_shapes(owner, pid, snippet_id=sid)
factory = next(r for r in rows if r.symbol == "factory")
assert factory.status == "instance" and factory.classified_by == "agent"
assert factory.reason == "confirmed symbol proposal (1.00)"
assert factory.proposal is None
# A judgment on a proposed row retires the proposal; withdrawing a
# judgment forgets the examination so the next pass proposes afresh.
await classify_shapes(owner, pid, [
{"path": "src/app.py", "symbol": "make_app", "status": "exempt", "reason": "bootstrap"},
])
rows, _ = await list_project_shapes(owner, pid, path="src/app.py")
make_app = next(r for r in rows if r.symbol == "make_app")
assert make_app.status == "exempt" and make_app.proposal is None
await classify_shapes(owner, pid, [
{"path": "src/app.py", "symbol": "make_app", "status": "unclassified"},
])
with _quiet_semantic():
assert (await propose_for_repo(owner, pid, REPO, defs))["proposed"] == 1
rows, _ = await list_project_shapes(owner, pid, proposal="canon")
assert [r.symbol for r in rows] == ["make_app"]
@pytest.mark.integration
async def test_derive_groups_land_on_rows_and_in_the_summary(seeded):
"""Shapes with no canon hit that repeat — identical bodies in two files,
the same name in three — carry a derive proposal, and the readout ranks
the families. A canon proposal keeps a row out of any derive group."""
from scribe.services.shape_ledger import (
apply_derive_groups, live_rows, propose_for_repo, proposal_summary,
)
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
defs = _defs(
("a/one.py", "sym", "slug", "def slug(t):", "def slug(t):\n return t.lower()"),
("a/two.py", "sym", "slug", "def slug(t):", "def slug(t):\n return t.lower()"),
("b/x.css", "css", "card", ".card {", ".card { padding: 1px }"),
("b/y.css", "css", "card", ".card {", ".card { padding: 2px }"),
("b/z.css", "css", "card", ".card {", ".card { padding: 3px }"),
("c/only.py", "sym", "alone", "def alone():", "def alone():\n return 0"),
("c/use.py", "sym", "boot", "def boot():", "def boot():\n return factory()"),
)
await sync_repo_shapes(pid, REPO, defs, seen_marker="main")
with _quiet_semantic():
await propose_for_repo(owner, pid, REPO, defs)
assert await apply_derive_groups(pid) == 5
rows, total = await list_project_shapes(owner, pid, proposal="derive")
assert total == 5
groups = {(r.path, r.symbol): r.proposal for r in rows}
assert groups[("a/one.py", "slug")]["group"] == groups[("a/two.py", "slug")]["group"]
assert groups[("a/one.py", "slug")]["group"].startswith("dup:")
assert groups[("b/x.css", "card")] == {"basis": "derive", "score": 3.0, "group": "name:css:card"}
rows, _ = await list_project_shapes(owner, pid, proposal="any")
assert {r.symbol for r in rows} == {"slug", "card", "boot"} # boot: reference proposal
summary = proposal_summary(await live_rows(pid))
assert summary["proposed"] == 1
assert [g["group"] for g in summary["derive_groups"]][0] == "name:css:card"
assert summary["derive_groups"][0]["label"] == ".card"
assert summary["derive_groups"][0]["size"] == 3
# One of the css copies gets judged → the group shrinks on the next pass.
await classify_shapes(owner, pid, [
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"},
])
await apply_derive_groups(pid)
rows, _ = await list_project_shapes(owner, pid, proposal="derive")
assert {r.symbol for r in rows} == {"slug"} # 2 files < the name floor
+51
View File
@@ -456,3 +456,54 @@ async def test_unservable_binding_measures_nothing(seeded):
await set_binding(uid, "https://github.com/somebody/else.git", other_pid)
assert await compute_coverage(uid, other_pid, selector=_selector(_tarball(TREE))) is None
# --- #2792: fingerprints and the proposer's readout --------------------------
def test_extract_definitions_fingerprints_each_block():
"""The block rule across the language families the extractor knows: a
Python def ends at the next top-level statement, a braces/CSS block keeps
its closer, and comments/decorators don't move the hash."""
from scribe.services.coverage import extract_definitions
text = (
"import os\n\n"
"def a(x):\n # comment\n return x + 1\n\n\n"
"class B:\n def m(self):\n return 2\n\n"
".btn {\n color: red;\n}\n"
"export const f = (x) => {\n return x;\n};\n"
)
defs = {d.name: d for d in extract_definitions(text)}
assert set(defs) == {"a", "B", "m", "btn", "f"}
assert defs["a"].signature == "def a(x):"
assert defs["a"].body.startswith("def a(x):\n # comment\n return x + 1")
assert "class B" not in defs["a"].body
assert defs["B"].body.rstrip().endswith("return 2")
assert defs["btn"].body == ".btn {\n color: red;\n}"
assert defs["f"].body == "export const f = (x) => {\n return x;\n};"
assert all(len(d.body_sha) == 16 for d in defs.values())
# Comment changes don't change what the shape IS; code changes do.
again = {d.name: d for d in extract_definitions(text.replace("# comment", "# other"))}
assert again["a"].body_sha == defs["a"].body_sha
changed = {d.name: d for d in extract_definitions(text.replace("x + 1", "x + 2"))}
assert changed["a"].body_sha != defs["a"].body_sha
# And the identity view is unchanged for the hook mirror.
from scribe.services.coverage import extract_shapes
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)]
def test_coverage_line_names_the_proposers_standing():
from scribe.services.coverage import coverage_line
base = {
"total": 100, "accounted": 10, "unclassified": 90,
"counts": {"canonical": 10, "instance": 0, "variant": 0, "exempt": 0},
"computed_at": "2026-08-21T00:00:00+00:00",
"largest_gaps": [{"dir": "src", "unclassified": 90, "total": 90}],
}
assert coverage_line(base).endswith("; 90 unclassified, largest: src")
line = coverage_line({**base, "proposed": 40, "derive_groups": [{"group": "a"}, {"group": "b"}]})
assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
assert "(1 derive group)" in line
+141
View File
@@ -5,6 +5,8 @@ token-free serialisation. The sync pass (step 2) and the classification
surface (step 3) grow their tests here; DB-backed behavior lands in the
integration lane once there is behavior to exercise.
"""
import pytest
from scribe.models import Base
from scribe.models.code_shape import SHAPE_CLASSIFIERS, SHAPE_STATUSES, CodeShape
@@ -135,3 +137,142 @@ def test_hook_is_a_server_internal_classifier():
assert "hook" in SHAPE_CLASSIFIERS
assert "hook" not in _CALLER_VIAS
# --- step 6: the mechanical proposer (pure) ---------------------------------
def test_proposal_columns_and_vocabulary_are_pinned():
"""Fingerprints + the proposer's standing suggestion live on the row; the
basis vocabulary is fixed, with `derive` the odd one out (a group, not a
snippet)."""
from scribe.models.code_shape import PROPOSAL_BASES
cols = CodeShape.__table__.c
for name in ("signature", "body_sha", "proposed_snippet_id", "proposal_basis",
"proposal_score", "proposal_group", "proposed_at", "proposed_sha"):
assert name in cols, name
fk = next(iter(cols.proposed_snippet_id.foreign_keys))
assert fk.ondelete == "SET NULL" and fk.column.table.name == "notes"
assert "ix_code_shapes_proposed" in {ix.name for ix in CodeShape.__table__.indexes}
assert PROPOSAL_BASES == ("symbol", "text", "reference", "signature", "semantic", "derive")
def test_row_proposal_property_is_one_object_or_none():
row = CodeShape(project_id=1, repo_key="r", path="a.py", symbol="f", kind="sym")
assert row.proposal is None
row.proposed_snippet_id, row.proposal_basis, row.proposal_score = 9, "symbol", 1.0
assert row.proposal == {"basis": "symbol", "score": 1.0, "snippet_id": 9}
row.proposed_snippet_id = None
row.proposal_basis, row.proposal_group, row.proposal_score = "derive", "dup:abc", 3.0
assert row.proposal == {"basis": "derive", "score": 3.0, "group": "dup:abc"}
def test_signature_similarity_blanks_the_names():
from scribe.services.shape_ledger import signature_similarity as sim
a = "def move_event(project_id: int, event_id: int, after_id: int | None):"
b = "def move_beat(project_id: int, beat_id: int, after_id: int | None):"
assert sim(a, "move_event", b, "move_beat") > 0.85
assert sim(a, "move_event", "def export_pdf(manuscript, design, fonts):", "export_pdf") < 0.6
assert sim("", "x", b, "move_beat") == 0.0
# Trivial signatures resemble everything and mean nothing — floored out.
assert sim("def helper(x):", "helper", "def make_app():", "make_app") == 0.0
def test_text_containment_is_whitespace_insensitive_with_a_floor():
from scribe.services.shape_ledger import text_contains
code = "const ok = await confirmed({ title: 'Delete?', confirmLabel: 'Delete' });\nif (!ok) return;"
body = "async function onDelete() {\n const ok = await confirmed({\n title: 'Delete?',\n confirmLabel: 'Delete'\n });\n if (!ok) return;\n}"
assert text_contains(body, code)
assert text_contains(code, body)
assert not text_contains("x = 1", "x = 1") # below the substance floor
def _canon(sid, kind="sym", symbol="", locations=(), signature="", code=""):
from scribe.services.shape_ledger import Canon, _norm_text
return Canon(sid, kind, symbol, tuple(locations), signature, _norm_text(code))
def test_match_canon_orders_bases_strongest_first_and_respects_kind():
from scribe.services.shape_ledger import match_canon
confirmed = _canon(
7, "sym", "confirmed", [("frontend/src/composables/useConfirm.ts", "confirmed")],
"export async function confirmed(opts: ConfirmOptions): Promise<boolean> {",
"export async function confirmed(opts: ConfirmOptions): Promise<boolean> { /* singleton */ }",
)
mover = _canon(
8, "sym", "move_beat", [("src/forge/plot.py", "move_beat")],
"def move_beat(project_id: int, beat_id: int, after_id: int | None) -> None:",
)
btn = _canon(9, "css", ".btn-primary", [("web/buttons.css", ".btn-primary")],
".btn-primary {", ".btn-primary { color: var(--action-primary); padding: 4px 8px; border-radius: 4px; }")
canons = [confirmed, mover, btn]
# symbol: a second `confirmed` defined elsewhere answers to #7 — but the
# canon's own location never does (that row is canonical, not a proposal).
assert match_canon("sym", "src/other.ts", "confirmed", "function confirmed() {", "", canons) == (7, "symbol", 1.0)
assert match_canon("sym", "frontend/src/composables/useConfirm.ts", "confirmed",
"export async function confirmed(", "", canons) is None
# reference: a call site of the canon.
body = "async function onTrash() {\n const ok = await confirmed({ title: 'x' });\n if (!ok) return;\n}"
assert match_canon("sym", "c.vue", "onTrash", "async function onTrash() {", body, canons) == (7, "reference", 0.9)
# signature: the family shape, names blanked.
hit = match_canon("sym", "src/forge/timeline.py", "move_event",
"def move_event(project_id: int, event_id: int, after_id: int | None) -> None:",
" pass", canons)
assert hit and hit[0] == 8 and hit[1] == "signature" and hit[2] >= 0.8
# text: the canon's code contains the shape's body (a css copy), kind-matched —
# the same text as a `sym` shape matches no css canon.
css_body = ".btn-primary { color: var(--action-primary); padding: 4px 8px; border-radius: 4px; }"
assert match_canon("css", "web/other.css", "btn-big", ".btn-big {", css_body, canons) == (9, "text", 0.95)
assert match_canon("sym", "web/other.css", "btn-big", ".btn-big {", css_body, canons) is None
# nothing in play
assert match_canon("sym", "x.py", "unrelated", "def unrelated(a, b, c, d, e):", "return 1", canons) is None
def test_match_canon_symbol_beats_everything_including_css_copies():
"""The previous test's css `btn-primary`-elsewhere case, stated plainly:
a second definition of the canon's own name is the symbol basis."""
from scribe.services.shape_ledger import match_canon
btn = _canon(9, "css", ".btn-primary", [("web/buttons.css", ".btn-primary")], ".btn-primary {", ".btn-primary { color: red; padding: 4px 8px; border-radius: 4px; }")
assert match_canon("css", "web/other.css", "btn-primary", ".btn-primary {", ".btn-primary { color: blue }", [btn]) == (9, "symbol", 1.0)
def test_derive_groups_copy_before_name_with_floors():
from scribe.services.shape_ledger import derive_groups
rows = [
("a.py", "sym", "helper", "sha1"), ("b.py", "sym", "helper", "sha1"), # identical copies
("c.py", "sym", "helper", "sha9"), # same name, 3rd file
("d.css", "css", "btn", "s1"), ("e.css", "css", "btn", "s2"), ("f.css", "css", "btn", "s3"),
("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group
("i.py", "sym", "one", "s6"),
]
g = derive_groups(rows)
assert g[("a.py", "sym", "helper")] == "dup:sha1" == g[("b.py", "sym", "helper")]
assert g[("c.py", "sym", "helper")] == "name:sym:helper"
assert g[("d.css", "css", "btn")] == "name:css:btn"
assert ("g.py", "sym", "main") not in g
assert ("i.py", "sym", "one") not in g
def test_confirm_requires_a_named_scope():
import asyncio
from scribe.services.shape_ledger import confirm_proposals
with pytest.raises(ValueError) as err:
asyncio.run(confirm_proposals(1, 2))
assert "name what you reviewed" in str(err.value)
def test_proposer_tools_are_mounted():
from scribe.mcp.server import build_mcp_server
mcp = build_mcp_server()
assert mcp._tool_manager.get_tool("confirm_shape_proposals") is not None
tool = mcp._tool_manager.get_tool("list_shapes")
assert "proposal" in tool.parameters.get("properties", {})