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
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:
@@ -15,6 +15,7 @@ from scribe.models.project import Project
|
||||
from scribe.models.user import User
|
||||
from scribe.services.shape_ledger import (
|
||||
classify_shapes,
|
||||
classify_shapes_where,
|
||||
list_project_shapes,
|
||||
snippet_consumers,
|
||||
sync_repo_shapes,
|
||||
@@ -128,6 +129,46 @@ async def test_classification_is_write_gated_and_listing_read_gated(seeded):
|
||||
assert await list_project_shapes(other, pid) == ([], 0)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded):
|
||||
"""#2868: one judgment over a directory + glob; judged rows are left
|
||||
alone unless include_judged; the same gates as the row form."""
|
||||
owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
|
||||
await classify_shapes(owner, pid, [
|
||||
{"path": "src/app.py", "symbol": "Config", "status": "exempt", "reason": "settings holder"},
|
||||
])
|
||||
out = await classify_shapes_where(
|
||||
owner, pid, path="src", status="instance", snippet_id=sid, via="audit",
|
||||
)
|
||||
# make_app + helper swept; Config (already judged) untouched; css not under src/.
|
||||
assert out["classified"] == 2
|
||||
assert out["sample"] == ["src/app.py::make_app", "src/util.py::helper"]
|
||||
rows, _ = await list_project_shapes(owner, pid)
|
||||
by_symbol = {r.symbol: r for r in rows}
|
||||
assert by_symbol["make_app"].status == "instance" and by_symbol["make_app"].classified_by == "audit"
|
||||
assert by_symbol["Config"].status == "exempt" and by_symbol["Config"].reason == "settings holder"
|
||||
assert by_symbol["btn"].status == "unclassified"
|
||||
# Glob + kind narrow; include_judged re-judges.
|
||||
out = await classify_shapes_where(
|
||||
owner, pid, path="web", status="exempt", pattern="btn*", kind="css",
|
||||
reason="one toolbar button", include_judged=True,
|
||||
)
|
||||
assert out["classified"] == 1
|
||||
out = await classify_shapes_where(
|
||||
owner, pid, path="src", status="unclassified", include_judged=True,
|
||||
)
|
||||
assert out["classified"] == 3 # withdrawal sweeps judged rows when asked
|
||||
# Gates: reason for exempt, snippet for instance, write access, a path.
|
||||
with pytest.raises(ValueError):
|
||||
await classify_shapes_where(owner, pid, path="src", status="exempt")
|
||||
with pytest.raises(ValueError):
|
||||
await classify_shapes_where(owner, pid, path="src", status="instance")
|
||||
with pytest.raises(ValueError):
|
||||
await classify_shapes_where(owner, pid, path="", status="exempt", reason="x")
|
||||
with pytest.raises(ValueError):
|
||||
await classify_shapes_where(other, pid, path="src", status="exempt", reason="x")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_list_filters_compose(seeded):
|
||||
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
|
||||
|
||||
@@ -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) ----------------------------------
|
||||
|
||||
Reference in New Issue
Block a user