feat(ledger): audit surfaces — list_shapes(compact=True) and classify_shapes_by_rule, the sweep form of a judgment (#2868, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 26s

The 2026-08 audit judged 3,427 rows in 14 hand-driven batches through a raw
MCP client because a list_shapes page overflowed the tool budget and every
row had to be sent back one by one. Now:

- list_shapes(compact=True): path · symbol · kind · status · signature, plus
  snippet_id / by / proposal / diverges_from / recheck only when set. A full
  500-row page fits. CodeShape.to_compact() is the row shape.
- classify_shapes_by_rule(project_id, path, status, pattern=, kind=,
  snippet_id=, reason=, via=, include_judged=): ONE judgment over every
  unclassified live row under a directory whose symbol matches a glob;
  judged rows are untouched unless include_judged; canonical is refused;
  same gates as the row form; one transaction; returns count + sample.
  shape_ledger.classify_shapes_where / rule_matches (pure) carry it.

Tests: compact row pinned, rule_matches directory/glob/kind semantics, the
tool mount, and an integration sweep (unclassified-only, include_judged,
gates).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:02:55 -04:00
co-authored by Claude Fable 5
parent 3a4031d7f8
commit 9abc4443fb
5 changed files with 255 additions and 3 deletions
+48
View File
@@ -334,6 +334,54 @@ def test_proposer_tools_are_mounted():
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", {})
# #2868: the audit surfaces — compact pages and the sweep form.
assert "compact" in tool.parameters.get("properties", {})
rule = mcp._tool_manager.get_tool("classify_shapes_by_rule")
assert rule is not None
for name in ("path", "status", "pattern", "kind", "snippet_id", "reason", "include_judged"):
assert name in rule.parameters.get("properties", {}), name
# --- #2868: the bulk surfaces (pure) -----------------------------------------
def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
"""A 500-row compact page must fit the tool budget: no commits, shas or
timestamps; optional fields only when set."""
from scribe.models.code_shape import CodeShape
row = CodeShape(project_id=2, repo_key="r", path="src/a.py", symbol="f", kind="sym",
status="unclassified", signature="def f(x):", body_sha="abc",
first_seen_commit="c1", last_seen_commit="c2")
assert row.to_compact() == {
"path": "src/a.py", "symbol": "f", "kind": "sym",
"status": "unclassified", "signature": "def f(x):",
}
row.status, row.snippet_id, row.classified_by = "instance", 9, "audit"
row.proposed_snippet_id, row.proposal_basis, row.proposal_score = 9, "symbol", 1.0
compact = row.to_compact()
assert compact["snippet_id"] == 9 and compact["by"] == "audit"
assert compact["proposal"]["basis"] == "symbol"
for noisy in ("first_seen_commit", "last_seen_commit", "body_sha", "created_at", "classified_at"):
assert noisy not in compact
def test_rule_matches_is_directory_glob_and_kind_aware():
from scribe.models.code_shape import CodeShape
from scribe.services.shape_ledger import rule_matches
def row(path, symbol, kind="sym"):
return CodeShape(project_id=2, repo_key="r", path=path, symbol=symbol, kind=kind, status="unclassified")
r = row("frontend/src/views/LoginView.vue", "auth-card", "css")
assert rule_matches(r, path="frontend/src/views", pattern="", kind="")
assert rule_matches(r, path="frontend/src/views", pattern="auth-*", kind="css")
assert not rule_matches(r, path="frontend/src/views", pattern="auth-*", kind="sym")
assert not rule_matches(r, path="frontend/src/view", pattern="", kind="") # directory, not prefix
assert rule_matches(r, path="frontend/src/views/LoginView.vue", pattern="", kind="")
# CSS symbols compare without the leading dot, like everywhere else.
assert rule_matches(row("w/a.css", ".btn-primary", "css"), path="w", pattern="btn-*", kind="css")
assert rule_matches(row("src/scribe/services/backup.py", "_note_rows"), path="src/scribe/services", pattern="_*_rows", kind="")
assert not rule_matches(row("src/scribe/services/backup.py", "export_full_backup"), path="src/scribe/services", pattern="_*_rows", kind="")
# --- step 7: the divergence readout (pure) ----------------------------------