{"version":3,"file":"trace-Cy8quQTm.mjs","names":[],"sources":["../src/graph/trace.ts"],"sourcesContent":["/**\n * trace.ts — graph_trace endpoint handler\n *\n * Given a topic/keyword query, searches vault notes for appearances of that topic\n * and returns a chronological timeline showing how the idea evolved over time.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphTraceParams {\n  query: string;\n  project_id: number;\n  max_results?: number;\n  lookback_days?: number;\n}\n\nexport interface TraceEntry {\n  vault_path: string;\n  title: string;\n  folder: string;\n  indexed_at: number;\n  snippet: string;\n  dominant_type: string;\n}\n\nexport interface TraceConnection {\n  from_path: string;\n  to_path: string;\n  type: \"temporal\" | \"wikilink\";\n}\n\nexport interface GraphTraceResult {\n  query: string;\n  entries: TraceEntry[];\n  connections: TraceConnection[];\n  time_span: { from: number; to: number };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n  const lastSlash = vaultPath.lastIndexOf(\"/\");\n  return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction extractSnippet(text: string, query: string): string {\n  const lowerText = text.toLowerCase();\n  const lowerQuery = query.toLowerCase();\n  const idx = lowerText.indexOf(lowerQuery);\n  if (idx === -1) {\n    return text.slice(0, 160).trimEnd() + (text.length > 160 ? \"…\" : \"\");\n  }\n\n  const CONTEXT = 70;\n  const start = Math.max(0, idx - CONTEXT);\n  const end = Math.min(text.length, idx + query.length + CONTEXT);\n\n  let snippet = text.slice(start, end).replace(/\\s+/g, \" \").trim();\n  if (start > 0) snippet = \"…\" + snippet;\n  if (end < text.length) snippet = snippet + \"…\";\n\n  return snippet;\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphTrace(\n  backend: StorageBackend,\n  params: GraphTraceParams\n): Promise<GraphTraceResult> {\n  const query = (params.query ?? \"\").trim();\n  if (!query) {\n    return { query: \"\", entries: [], connections: [], time_span: { from: 0, to: 0 } };\n  }\n\n  const maxResults = params.max_results ?? 30;\n  const lookbackDays = params.lookback_days ?? 365;\n  const cutoffTimestamp = Math.floor(Date.now() / 1000) - lookbackDays * 86400;\n\n  // ---------------------------------------------------------------------------\n  // Step 1: Collect matching vault paths via multiple search strategies\n  // ---------------------------------------------------------------------------\n\n  const matchedPaths = new Map<string, string>(); // path → best snippet text\n\n  // Strategy A: title / alias match in vault_name_index\n  try {\n    const namePaths = await backend.searchVaultNameIndex(query, 100);\n    for (const vaultPath of namePaths) {\n      if (!matchedPaths.has(vaultPath)) {\n        matchedPaths.set(vaultPath, \"\");\n      }\n    }\n  } catch {\n    // vault_name_index may not exist in all schema versions\n  }\n\n  // Strategy B: content search in memory_chunks\n  try {\n    const chunkRows = await backend.searchChunksByText(params.project_id, query, 200);\n    for (const row of chunkRows) {\n      if (!matchedPaths.has(row.path)) {\n        matchedPaths.set(row.path, row.text);\n      } else if (!matchedPaths.get(row.path)) {\n        matchedPaths.set(row.path, row.text);\n      }\n    }\n  } catch {\n    // Best-effort — continue with what we have\n  }\n\n  if (matchedPaths.size === 0) {\n    return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n  }\n\n  // ---------------------------------------------------------------------------\n  // Step 2: Fetch vault_files metadata for all matched paths, filtered by cutoff\n  // ---------------------------------------------------------------------------\n\n  const allPaths = Array.from(matchedPaths.keys());\n  let fileRows: Array<{ vaultPath: string; title: string | null; indexedAt: number }> = [];\n  try {\n    fileRows = await backend.getVaultFilesByPathsAfter(allPaths, cutoffTimestamp * 1000);\n    // Sort chronologically\n    fileRows.sort((a, b) => a.indexedAt - b.indexedAt);\n  } catch {\n    return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n  }\n\n  if (fileRows.length === 0) {\n    return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n  }\n\n  // ---------------------------------------------------------------------------\n  // Step 3: Build TraceEntry array\n  // ---------------------------------------------------------------------------\n\n  const entries: TraceEntry[] = fileRows.slice(0, maxResults).map((row) => {\n    const fileName = row.vaultPath.split(\"/\").pop() ?? row.vaultPath;\n    const title = row.title ?? fileName.replace(/\\.md$/i, \"\");\n    const chunkText = matchedPaths.get(row.vaultPath) ?? \"\";\n    const snippet = extractSnippet(chunkText, query);\n\n    return {\n      vault_path: row.vaultPath,\n      title,\n      folder: folderFromPath(row.vaultPath),\n      indexed_at: row.indexedAt,\n      snippet,\n      dominant_type: \"unknown\",\n    };\n  });\n\n  if (entries.length === 0) {\n    return { query, entries: [], connections: [], time_span: { from: 0, to: 0 } };\n  }\n\n  // ---------------------------------------------------------------------------\n  // Step 4: Build connections\n  // ---------------------------------------------------------------------------\n\n  const connections: TraceConnection[] = [];\n  const entryPathSet = new Set(entries.map((e) => e.vault_path));\n\n  // Temporal edges: consecutive entries (oldest → next)\n  for (let i = 0; i < entries.length - 1; i++) {\n    connections.push({\n      from_path: entries[i].vault_path,\n      to_path: entries[i + 1].vault_path,\n      type: \"temporal\",\n    });\n  }\n\n  // Wikilink edges\n  try {\n    const entryPaths = entries.map(e => e.vault_path);\n    const linkRows = await backend.getVaultLinksFromPaths(entryPaths);\n\n    const wikiEdgeKeys = new Set<string>();\n    for (const row of linkRows) {\n      if (!row.targetPath) continue;\n      if (!entryPathSet.has(row.targetPath)) continue;\n\n      const key = `${row.sourcePath}|||${row.targetPath}`;\n      if (wikiEdgeKeys.has(key)) continue;\n      wikiEdgeKeys.add(key);\n\n      connections.push({\n        from_path: row.sourcePath,\n        to_path: row.targetPath,\n        type: \"wikilink\",\n      });\n    }\n  } catch {\n    // vault_links may not exist — temporal edges are sufficient\n  }\n\n  // ---------------------------------------------------------------------------\n  // Step 5: Compute time span\n  // ---------------------------------------------------------------------------\n\n  const timestamps = entries.map((e) => e.indexed_at).filter((t) => t > 0);\n  const timeFrom = timestamps.length > 0 ? Math.min(...timestamps) : 0;\n  const timeTo = timestamps.length > 0 ? Math.max(...timestamps) : 0;\n\n  return {\n    query,\n    entries,\n    connections,\n    time_span: { from: timeFrom, to: timeTo },\n  };\n}\n"],"mappings":"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"}