From b33e2a79c647314e3a8659dbc6b720c107c50790 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 25 Jul 2026 19:00:05 -0400 Subject: [PATCH] fix(snippets): close the recall-surface gaps found reviewing the Drafter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four defects from the 2026-07-25 review of the recall (#227) and merge (#231) milestones. The theme: a snippet could be recorded but not fully corrected, and the agent and web surfaces had drifted apart. - #2076 language was mis-derived from the first caller tag, so a snippet created with tags and no language read that tag back as its language — corrupting the tag set and the code fence on the next update. Only the FIRST tag can carry the language, since compose_tags emits [language, "snippet", *caller]. - #2077 MCP update_snippet mapped "" to "unchanged", so no field could ever be cleared and no snippet detached from its project. Now an omitted field is left alone, an empty string clears, and project_id follows the -1 = detach convention. A service-level UNSET sentinel keeps None available as the clear. - #2078 surface parity: adds delete_snippet (MCP had none, so a wrong snippet could not be retired by the agent that recorded it), locations on MCP create and update, system_ids through the REST routes and the editor, and the near-duplicate gate on REST create with a "record it anyway" escape. - #2079 project scoping: list_snippets takes project_id through the service, the MCP tool and the REST route, defaulting to every project — reaching across projects is the point when the helper you need was written elsewhere. Sharing the list across owners is deliberately NOT in here: query_knowledge is shared with the Knowledge browse surface, so widening it changes behaviour well beyond snippets. Left open on #2079 for a scope decision. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt --- frontend/src/api/snippets.ts | 7 +- frontend/src/views/SnippetEditorView.vue | 167 ++++++++++++++++++++++- plugin/.claude-plugin/plugin.json | 2 +- plugin/skills/reusing-code/SKILL.md | 15 ++ src/scribe/mcp/server.py | 5 +- src/scribe/mcp/tools/snippets.py | 88 +++++++++--- src/scribe/routes/snippets.py | 62 ++++++++- src/scribe/services/knowledge.py | 12 +- src/scribe/services/snippets.py | 47 +++++-- tests/test_mcp_tool_snippets.py | 69 +++++++++- tests/test_routes_snippets.py | 41 +++++- tests/test_services_snippets.py | 20 +++ 12 files changed, 492 insertions(+), 43 deletions(-) diff --git a/frontend/src/api/snippets.ts b/frontend/src/api/snippets.ts index 40796f3..f625973 100644 --- a/frontend/src/api/snippets.ts +++ b/frontend/src/api/snippets.ts @@ -36,6 +36,7 @@ export interface Snippet { created_at: string; updated_at: string; snippet: SnippetFields; + systems?: { id: number; name: string }[]; } /** Lightweight list item from the knowledge preview feed. Note: the `snippet` @@ -63,14 +64,18 @@ export interface SnippetInput { locations?: SnippetLocation[]; tags?: string[]; project_id?: number | null; + system_ids?: number[]; + /** Create only: record it even though a near-duplicate already exists. */ + force?: boolean; } export async function listSnippets( - params: { q?: string; tag?: string } = {}, + params: { q?: string; tag?: string; projectId?: number | null } = {}, ): Promise<{ snippets: SnippetListItem[]; total: number }> { const qs = new URLSearchParams(); if (params.q) qs.set("q", params.q); if (params.tag) qs.set("tag", params.tag); + if (params.projectId) qs.set("project_id", String(params.projectId)); const query = qs.toString(); return apiGet(`/api/snippets${query ? `?${query}` : ""}`); } diff --git a/frontend/src/views/SnippetEditorView.vue b/frontend/src/views/SnippetEditorView.vue index 2b0f5af..7420b6d 100644 --- a/frontend/src/views/SnippetEditorView.vue +++ b/frontend/src/views/SnippetEditorView.vue @@ -1,5 +1,5 @@