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:
@@ -370,6 +370,82 @@ async def classify_shapes(
|
||||
return {"classified": classified, "unmatched": unmatched}
|
||||
|
||||
|
||||
def rule_matches(row: CodeShape, *, path: str, pattern: str, kind: str) -> bool:
|
||||
"""Does a ledger row fall under a rule-form classification (#2868)?
|
||||
``path`` is a file or a directory (everything beneath it), ``pattern``
|
||||
a shell glob on the symbol (``""`` = every symbol), ``kind`` narrows to
|
||||
sym/css. Pure, so the sweep's reach can be tested without a database."""
|
||||
import fnmatch
|
||||
|
||||
clean = (path or "").strip().strip("/")
|
||||
if clean and not (row.path == clean or row.path.startswith(clean + "/")):
|
||||
return False
|
||||
if kind and row.kind != kind:
|
||||
return False
|
||||
if pattern and not fnmatch.fnmatchcase(_norm_symbol(row.symbol), pattern):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def classify_shapes_where(
|
||||
user_id: int,
|
||||
project_id: int,
|
||||
*,
|
||||
path: str,
|
||||
status: str,
|
||||
pattern: str = "",
|
||||
kind: str = "",
|
||||
snippet_id: int | None = None,
|
||||
reason: str | None = None,
|
||||
via: str = "agent",
|
||||
include_judged: bool = False,
|
||||
) -> dict:
|
||||
"""The sweep form of classify_shapes (#2868): one judgment applied to
|
||||
every live row under ``path`` whose symbol matches ``pattern`` (and
|
||||
``kind``). By default only `unclassified` rows are touched — a sweep
|
||||
must never silently overwrite a judgment; ``include_judged`` opts in.
|
||||
Same gates as the row form (status vocabulary, snippet target, reason
|
||||
for variant/exempt, write access); one transaction, so it applies whole
|
||||
or not at all. Returns the count and a sample of what it judged."""
|
||||
from scribe.services import access
|
||||
from scribe.services import snippets as snippets_svc
|
||||
|
||||
if via not in _CALLER_VIAS:
|
||||
raise ValueError(f"via must be one of: {', '.join(_CALLER_VIAS)}")
|
||||
if not (path or "").strip():
|
||||
raise ValueError("path is required — a sweep names the directory it judges")
|
||||
if status == "canonical":
|
||||
raise ValueError("canonical is the sync's stamp on a snippet's own location — a sweep cannot set it")
|
||||
probe = {"path": path, "symbol": "*", "status": status,
|
||||
"snippet_id": snippet_id or 0, "reason": reason or ""}
|
||||
error = validate_classifications([probe])
|
||||
if error:
|
||||
raise ValueError(error.replace("classifications[0]", "rule"))
|
||||
if not await access.can_write_project(user_id, project_id):
|
||||
raise ValueError(f"project {project_id} not found or no write access")
|
||||
if status in _NEEDS_TARGET and await snippets_svc.get_snippet(user_id, int(snippet_id)) is None:
|
||||
raise ValueError(f"snippet {snippet_id} not found (or not readable)")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
judged: list[str] = []
|
||||
async with async_session() as session:
|
||||
conds = [CodeShape.project_id == project_id, CodeShape.vanished_at.is_(None)]
|
||||
if not include_judged:
|
||||
conds.append(CodeShape.status == "unclassified")
|
||||
rows = (await session.execute(select(CodeShape).where(*conds))).scalars().all()
|
||||
for row in rows:
|
||||
if not rule_matches(row, path=path, pattern=pattern, kind=kind):
|
||||
continue
|
||||
await _judge(
|
||||
session, row, status=status,
|
||||
snippet_id=int(snippet_id) if status in _NEEDS_TARGET else None,
|
||||
by=via, reason=reason, at=now,
|
||||
)
|
||||
judged.append(f"{row.path}::{row.symbol}")
|
||||
await session.commit()
|
||||
return {"classified": len(judged), "sample": sorted(judged)[:12]}
|
||||
|
||||
|
||||
async def list_project_shapes(
|
||||
user_id: int,
|
||||
project_id: int,
|
||||
|
||||
Reference in New Issue
Block a user