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
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:
@@ -50,3 +50,35 @@ def test_status_queries_have_an_index():
|
||||
names = {ix.name for ix in CodeShape.__table__.indexes}
|
||||
assert "ix_code_shapes_project_status" in names
|
||||
assert "ix_code_shapes_snippet" in names
|
||||
|
||||
|
||||
# --- step 3: the classification batch validator (pure, checked before ACL) ---
|
||||
|
||||
|
||||
def test_batch_validation_names_the_failing_item():
|
||||
from scribe.services.shape_ledger import validate_classifications as v
|
||||
|
||||
ok = {"path": "src/a.py", "symbol": "f", "status": "exempt", "reason": "one-off"}
|
||||
assert v([ok]) is None
|
||||
assert "empty" in v([])
|
||||
assert "classifications[1]" in v([ok, {"symbol": "f", "status": "exempt"}])
|
||||
assert "unknown status" in v([{**ok, "status": "covered"}])
|
||||
# A judgment that references canon must name the canon...
|
||||
assert "needs snippet_id" in v(
|
||||
[{"path": "a", "symbol": "f", "status": "instance"}]
|
||||
)
|
||||
# ...and a departure/exemption must carry its why — the why IS the record.
|
||||
assert "needs a reason" in v(
|
||||
[{"path": "a", "symbol": "f", "status": "variant", "snippet_id": 3}]
|
||||
)
|
||||
assert "needs a reason" in v([{"path": "a", "symbol": "f", "status": "exempt"}])
|
||||
# Withdrawing a judgment needs neither target nor reason.
|
||||
assert v([{"path": "a", "symbol": "f", "status": "unclassified"}]) is None
|
||||
|
||||
|
||||
def test_classify_and_list_are_mounted_as_mcp_tools():
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
|
||||
mcp = build_mcp_server()
|
||||
for name in ("classify_shapes", "list_shapes"):
|
||||
assert mcp._tool_manager.get_tool(name) is not None
|
||||
|
||||
Reference in New Issue
Block a user