diff --git a/frontend/src/api/snippets.ts b/frontend/src/api/snippets.ts index e912d4c..249bd30 100644 --- a/frontend/src/api/snippets.ts +++ b/frontend/src/api/snippets.ts @@ -56,6 +56,19 @@ export interface SnippetUsage { last_pulled_at: string | null; } +/** Result of the last drift check — does the recorded location and code still + * match source? The check runs agent-side (Scribe has no checkout); this is the + * remembered verdict. `current` is false once the snippet has been edited since + * the check, at which point the verdict describes code that's no longer there. */ +export interface SnippetVerification { + status: "ok" | "missing" | "moved" | "changed" | "unverified"; + current: boolean; + checked_at: string | null; + detail?: string | null; + path?: string | null; + needs_attention?: boolean; +} + /** Lightweight list item from the knowledge preview feed. Note: the `snippet` * field here is a truncated *body preview* (the knowledge feed's naming), not * the parsed fields above. */ @@ -73,6 +86,9 @@ export interface SnippetListItem { owner?: string | null; /** Always present from the backend, zero-filled for records with no events. */ usage?: SnippetUsage; + /** Present on the detail record; the list feed carries it when a check has + * been recorded. */ + verification?: SnippetVerification; } /** Create/update payload — discrete fields the backend serializes into the @@ -103,6 +119,8 @@ export async function listSnippets( repo?: string; path?: string; symbol?: string; + /** Drift check: "attention" | "ok" | "unverified" | "drifted" | a status. */ + verification?: string; } = {}, ): Promise<{ snippets: SnippetListItem[]; total: number }> { const qs = new URLSearchParams(); @@ -112,6 +130,7 @@ export async function listSnippets( if (params.repo) qs.set("repo", params.repo); if (params.path) qs.set("path", params.path); if (params.symbol) qs.set("symbol", params.symbol); + if (params.verification) qs.set("verification", params.verification); const query = qs.toString(); return apiGet(`/api/snippets${query ? `?${query}` : ""}`); } @@ -135,6 +154,16 @@ export async function deleteSnippet(id: number): Promise { return apiDelete(`/api/snippets/${id}`); } +/** Record a drift-check verdict. The check itself runs where the code is — an + * agent with the working tree — since Scribe has no checkout. This stores what + * was found, and is how the UI clears a stale marker after a manual fix. */ +export async function verifySnippet( + id: number, + verdict: { status: string; detail?: string; path?: string }, +): Promise { + return apiPost(`/api/snippets/${id}/verify`, verdict); +} + /** Unify `sourceIds` into the canonical snippet `targetId`. Returns the merged * survivor plus `merged_ids` — the sources actually folded in and trashed. */ export async function mergeSnippets( diff --git a/frontend/src/views/SnippetListView.vue b/frontend/src/views/SnippetListView.vue index 2af502f..352ca4b 100644 --- a/frontend/src/views/SnippetListView.vue +++ b/frontend/src/views/SnippetListView.vue @@ -34,6 +34,15 @@ function toggleLocationFilter() { if (!showLocationFilter.value && locationActive.value) clearLocation(); } +// Drift check (#2086) — "attention" is everything whose recorded location or +// code no longer checks out, plus everything whose verdict expired because the +// snippet was edited since it was checked. +const needsAttentionOnly = ref(false); +function toggleAttention() { + needsAttentionOnly.value = !needsAttentionOnly.value; + loadSnippets(); +} + // Multi-select → merge const selectMode = ref(false); const selectedIds = ref>(new Set()); @@ -96,6 +105,7 @@ async function loadSnippets() { repo: locRepo.value.trim() || undefined, path: locPath.value.trim() || undefined, symbol: locSymbol.value.trim() || undefined, + verification: needsAttentionOnly.value ? "attention" : undefined, }); snippets.value = data.snippets; } catch { @@ -144,6 +154,39 @@ function usageBadge(s: SnippetListItem): string { return `${u.pull_count}/${u.surfaced_count} used`; } +/** Short label for the drift verdict, or "" when there's nothing to say. + * An expired verdict is reported as "unchecked" whatever it used to say — + * it was about code that is no longer in the record. */ +function driftBadge(s: SnippetListItem): string { + const v = s.verification; + if (!v) return ""; + if (!v.current) return "unchecked since edit"; + return { ok: "", missing: "path gone", moved: "symbol moved", changed: "code drifted" }[ + v.status + ] ?? ""; +} + +function driftTitle(s: SnippetListItem): string { + const v = s.verification; + if (!v) return ""; + const when = v.checked_at + ? `Checked ${new Date(v.checked_at).toLocaleDateString()}` + : "Checked"; + if (!v.current) { + return ( + `${when}, but the snippet has been edited since — that verdict was about ` + + `code this record no longer holds. Re-verify it.` + ); + } + const what = + { + missing: "the recorded path no longer exists", + moved: "the file is there but the symbol isn't in it", + changed: "the source no longer matches the recorded code", + }[v.status] ?? ""; + return v.detail ? `${when}: ${what}. ${v.detail}` : `${when}: ${what}.`; +} + function usageTitle(s: SnippetListItem): string { const u = s.usage; if (!u) return ""; @@ -201,6 +244,15 @@ function usageTitle(s: SnippetListItem): string { a screen reader too. --> {{ locationActive ? "Location · filtering" : "Location" }} + @@ -243,20 +295,27 @@ function usageTitle(s: SnippetListItem): string {
❭_

- {{ locationActive - ? "Nothing kept at that location yet" - : search.trim() - ? "No snippets match your search" - : "No snippets kept yet" }} + {{ needsAttentionOnly + ? "Everything checks out" + : locationActive + ? "Nothing kept at that location yet" + : search.trim() + ? "No snippets match your search" + : "No snippets kept yet" }}

- {{ locationActive - ? "No recorded snippet lives there — so whatever you're about to write is new. Widen the path, or clear the filter." - : search.trim() - ? "Try a different term, or clear the search." - : "Record a reusable function or component and it will be offered back to you later." }} + {{ needsAttentionOnly + ? "No snippet has drifted from its recorded location or code — as far as anything has been checked. Snippets nobody has verified yet don't appear here." + : locationActive + ? "No recorded snippet lives there — so whatever you're about to write is new. Widen the path, or clear the filter." + : search.trim() + ? "Try a different term, or clear the search." + : "Record a reusable function or component and it will be offered back to you later." }}

- +