import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client"; /** One canonical location of a reusable thing. A snippet that unifies several * one-offs carries several — one per call site. */ export interface SnippetLocation { repo: string; path: string; symbol: string; } /** Structured fields parsed out of a snippet note (mirrors the backend * `parse_snippet_fields` — see services/snippets.py). `repo`/`path`/`symbol` * mirror the first location for back-compat; `locations` is the full list. */ export interface SnippetFields { name: string; when_to_use: string; signature: string; language: string; repo: string; path: string; symbol: string; locations: SnippetLocation[]; /** The snippets folded into this one by merge, oldest first. Read-only: merge * is the only thing that adds to it, and an edit carries it forward. * * Each entry records what THAT source contributed — never what the survivor * already had — which is what lets un-merge subtract exactly. An entry with * no `locations`/`tags` predates that attribution and cannot be un-merged. */ merged_from: { id: number; locations?: SnippetLocation[]; tags?: string[] }[]; code: string; } /** A full snippet record: the note dict plus the parsed `snippet` sub-object, * as returned by the backend `snippet_to_dict`. */ export interface Snippet { id: number; title: string; body: string; tags: string[]; note_type: string; project_id: number | null; permission?: string; created_at: string; updated_at: string; snippet: SnippetFields; systems?: { id: number; name: string }[]; /** Set when another user owns this record; `owner` is their username and * `permission` your access level on it. */ shared?: boolean; owner?: string | null; } /** How often a record was put in front of an agent versus actually opened. * A high `surfaced_count` with `pull_count: 0` is dead weight — it occupies a * slot in every future auto-inject menu while never being used. */ export interface SnippetUsage { surfaced_count: number; pull_count: number; last_surfaced_at: string | null; 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. */ export interface SnippetListItem { id: number; title: string; tags: string[]; note_type: string; snippet: string; created_at: string; updated_at: string; /** Present only when the record belongs to someone else — a suggestion from * them, not one of your own. Absent means it's yours. */ shared?: boolean; 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 * note (title/body/tags). Empty strings are applied; omitted keys are left * unchanged on update. */ export interface SnippetInput { name: string; code: string; language?: string; signature?: string; when_to_use?: string; locations?: SnippetLocation[]; tags?: string[]; project_id?: number | null; system_ids?: number[]; /** Create only: record it even though a near-duplicate already exists. */ force?: boolean; } /** `repo`/`path`/`symbol` are the reverse lookup — "what already lives here?" * They must all match the same recorded location; `path` also matches anything * beneath it. Combinable with `q`. */ export async function listSnippets( params: { q?: string; tag?: string; projectId?: number | null; 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(); 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)); 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}` : ""}`); } export async function getSnippet(id: number): Promise { return apiGet(`/api/snippets/${id}`); } export async function createSnippet(data: SnippetInput): Promise { return apiPost("/api/snippets", data); } export async function updateSnippet( id: number, data: Partial, ): Promise { return apiPatch(`/api/snippets/${id}`, data); } export async function deleteSnippet(id: number): Promise { return apiDelete(`/api/snippets/${id}`); } /** A set of snippets that resemble each other closely enough to be worth * merging. Grouping is transitive, so a set can hold members that don't * directly resemble each other — read it as a proposal, not a verdict. */ export interface DuplicateGroup { note_ids: number[]; snippets: { id: number; title: string }[]; /** The strongest resemblance within the set — how confident the suggestion is. */ top_score: number; } /** Near-duplicates already in the record. The create gate prevents new ones and * merge cures the ones you point it at; this is what finds them. */ export async function findDuplicateSnippets( threshold?: number, ): Promise<{ groups: DuplicateGroup[]; threshold: number }> { const qs = threshold ? `?threshold=${threshold}` : ""; return apiGet(`/api/snippets/duplicates${qs}`); } /** 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); } /** Pull one source back out of a merged survivor: restores it and strips exactly * what it contributed. Also repairs a half-undone merge — a source restored * from the trash by hand leaves the survivor still claiming its call sites. */ export async function unmergeSnippet( survivorId: number, sourceId: number, ): Promise<{ survivor: Snippet; restored: Snippet | null }> { return apiPost(`/api/snippets/${survivorId}/unmerge`, { source_id: sourceId }); } /** 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( targetId: number, sourceIds: number[], ): Promise { return apiPost(`/api/snippets/${targetId}/merge`, { source_ids: sourceIds }); }