"""Shape-ledger MCP tools — the classification write/read surface (#2789). The accounting model (note 2786): the snippet library records CANON (small); the ledger accounts for EVERY extracted shape (total). These tools are how agents move shapes out of `unclassified` — the todo state — and how they read what still needs judgment. The ledger rows themselves are fed by the coverage refresh; these tools only ever judge what the sync has seen. """ from __future__ import annotations from scribe.mcp._context import current_user_id from scribe.services import coverage as coverage_svc from scribe.services import shape_ledger as shape_ledger_svc async def classify_shapes( project_id: int, classifications: list[dict], via: str = "agent" ) -> dict: """Record judgments for a project's code shapes — in batch, as rows. EVERY shape in a bound repo should end up classified (note 2786): - `instance` of snippet N — it conforms to recorded canon (family-level canon in another project counts; that fully accounts for the shape). - `variant` of snippet N — a deliberate, named departure. `reason` (the why) is REQUIRED; it is the record. - `exempt` — judged genuinely one-off. `reason` REQUIRED. - `canonical` of snippet N — this row IS the snippet's reference (rarely set by hand; the coverage sync stamps these mechanically). - `unclassified` — withdraw a judgment; the shape rejoins the todo. Consumer maps belong HERE, not in prose: when an audit enumerates call sites of a canonical helper, each call site's defining shape is an `instance` row — a sentence in a verification detail cannot be sorted, queried, or diffed. Args: project_id: The project whose ledger is being judged. classifications: Objects of {path, symbol, status, kind?, snippet_id?, reason?}. path+symbol name the shape exactly as list_shapes shows it; kind ("sym"/"css") narrows when one file defines both. snippet_id is required for canonical/instance/variant; reason is required for variant/exempt. via: Who is judging — "agent" (default), "audit" (a sweep), or "import" (carrying maps recorded elsewhere). All-or-nothing: a structural error, a missing snippet target, or no write access applies NOTHING. Returns {"classified": N, "unmatched": [...]} — unmatched names shapes no live ledger row matches (the tree may have moved since you listed; re-run the project's coverage refresh to re-sync). """ uid = current_user_id() return await shape_ledger_svc.classify_shapes( uid, project_id, classifications, via=via ) async def list_shapes( project_id: int, status: str = "", path: str = "", snippet_id: int = 0, include_vanished: bool = False, limit: int = 100, offset: int = 0, proposal: str = "", flag: str = "", compact: bool = False, ) -> dict: """Read a project's shape ledger — `status="unclassified"` IS the todo. Every extracted definition in the project's bound repos has a row here (fed by the coverage refresh). Filters compose: Args: status: canonical | instance | variant | exempt | scoped | unclassified. `scoped` (#2869) is the sync's mechanical stamp on one-offs by construction (a Vue component's scoped