{"version":3,"file":"note-context-DCcRWBoN.mjs","names":[],"sources":["../src/graph/note-context.ts"],"sourcesContent":["/**\n * note-context.ts — graph_note_context endpoint handler\n *\n * Given a single vault note path, returns ALL notes linked to or from it\n * across the entire vault (1-hop neighbourhood), plus the edges.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphNoteContextParams {\n  vault_path: string;\n  project_id: number;\n  max_neighbors?: number;\n  include_backlinks?: boolean;\n  include_outlinks?: boolean;\n}\n\nexport interface NoteNode {\n  vault_path: string;\n  title: string;\n  folder: string;\n  observation_types: Record<string, number>;\n  dominant_type: string;\n  updated_at: number;\n  word_count: number;\n}\n\nexport interface NoteEdge {\n  source: string;\n  target: string;\n  type: \"wikilink\" | \"semantic\";\n  weight: number;\n}\n\nexport interface GraphNoteContextResult {\n  focal: NoteNode;\n  neighbors: NoteNode[];\n  edges: NoteEdge[];\n  cluster_membership: Record<string, 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 dominantType(counts: Record<string, number>): string {\n  let best = \"unknown\";\n  let maxCount = 0;\n  for (const [type, n] of Object.entries(counts)) {\n    if (n > maxCount) {\n      maxCount = n;\n      best = type;\n    }\n  }\n  return best;\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment\n// ---------------------------------------------------------------------------\n\nasync function fetchObservationTypes(\n  pool: Pool,\n  filePaths: string[],\n  projectId: number\n): Promise<Map<string, Record<string, number>>> {\n  if (filePaths.length === 0) return new Map();\n\n  try {\n    const result = await pool.query<{ path: string; type: string; cnt: string }>(\n      `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n       FROM pai_observations,\n            LATERAL unnest(files_modified || files_read) AS unnested_path\n       WHERE unnested_path = ANY($1::text[])\n         AND project_id = $2\n       GROUP BY unnested_path, type`,\n      [filePaths, projectId]\n    );\n\n    const byPath = new Map<string, Record<string, number>>();\n    for (const row of result.rows) {\n      const existing = byPath.get(row.path) ?? {};\n      existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n      byPath.set(row.path, existing);\n    }\n    return byPath;\n  } catch {\n    return new Map();\n  }\n}\n\nfunction buildNoteNode(\n  vaultPath: string,\n  fileIndex: Map<string, { title: string | null; indexedAt: number }>,\n  obsByPath: Map<string, Record<string, number>>\n): NoteNode {\n  const fileRow = fileIndex.get(vaultPath);\n  const fileName = vaultPath.split(\"/\").pop() ?? vaultPath;\n  const rawTitle = fileRow?.title ?? fileName.replace(/\\.md$/i, \"\");\n  const obsCounts = obsByPath.get(vaultPath) ?? {};\n\n  return {\n    vault_path: vaultPath,\n    title: rawTitle,\n    folder: folderFromPath(vaultPath),\n    observation_types: obsCounts,\n    dominant_type: dominantType(obsCounts),\n    updated_at: fileRow?.indexedAt ?? 0,\n    word_count: 0,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphNoteContext(\n  pool: Pool | null,\n  backend: StorageBackend,\n  params: GraphNoteContextParams\n): Promise<GraphNoteContextResult> {\n  const focalPath = params.vault_path;\n  if (!focalPath) {\n    throw new Error(\"graph_note_context: vault_path is required\");\n  }\n\n  const maxNeighbors = params.max_neighbors ?? 50;\n  const includeBacklinks = params.include_backlinks !== false;\n  const includeOutlinks = params.include_outlinks !== false;\n\n  // -------------------------------------------------------------------------\n  // 1. Collect 1-hop neighbor paths via vault_links\n  // -------------------------------------------------------------------------\n\n  const neighborPaths = new Set<string>();\n  const rawEdges: Array<{ source: string; target: string }> = [];\n\n  if (includeOutlinks) {\n    const outLinks = await backend.getLinksFromSource(focalPath);\n    for (const link of outLinks) {\n      if (!link.targetPath) continue;\n      neighborPaths.add(link.targetPath);\n      rawEdges.push({ source: focalPath, target: link.targetPath });\n    }\n  }\n\n  if (includeBacklinks) {\n    const inLinks = await backend.getLinksToTarget(focalPath);\n    for (const link of inLinks) {\n      neighborPaths.add(link.sourcePath);\n      rawEdges.push({ source: link.sourcePath, target: focalPath });\n    }\n  }\n\n  // Cap neighbors at max_neighbors, keeping the most-linked ones\n  let neighborPathList = Array.from(neighborPaths);\n  if (neighborPathList.length > maxNeighbors) {\n    const linkCount = new Map<string, number>();\n    for (const e of rawEdges) {\n      const neighbor = e.source === focalPath ? e.target : e.source;\n      linkCount.set(neighbor, (linkCount.get(neighbor) ?? 0) + 1);\n    }\n    neighborPathList = neighborPathList\n      .sort((a, b) => (linkCount.get(b) ?? 0) - (linkCount.get(a) ?? 0))\n      .slice(0, maxNeighbors);\n  }\n\n  const retainedSet = new Set(neighborPathList);\n  const retainedEdges = rawEdges.filter((e) => {\n    const neighbor = e.source === focalPath ? e.target : e.source;\n    return retainedSet.has(neighbor);\n  });\n\n  // -------------------------------------------------------------------------\n  // 2. Fetch vault_files metadata for focal + all neighbors\n  // -------------------------------------------------------------------------\n\n  const allPaths = [focalPath, ...neighborPathList];\n  const fileRows = await backend.getVaultFilesByPaths(allPaths);\n  const fileIndex = new Map<string, { title: string | null; indexedAt: number }>(\n    fileRows.map(f => [f.vaultPath, { title: f.title, indexedAt: f.indexedAt }])\n  );\n\n  // -------------------------------------------------------------------------\n  // 3. Observation type enrichment (Postgres if available)\n  // -------------------------------------------------------------------------\n\n  const obsByPath =\n    pool !== null\n      ? await fetchObservationTypes(pool, allPaths, params.project_id)\n      : new Map<string, Record<string, number>>();\n\n  // -------------------------------------------------------------------------\n  // 4. Build focal NoteNode\n  // -------------------------------------------------------------------------\n\n  const focal = buildNoteNode(focalPath, fileIndex, obsByPath);\n\n  // -------------------------------------------------------------------------\n  // 5. Build neighbor NoteNode array\n  // -------------------------------------------------------------------------\n\n  const neighbors: NoteNode[] = neighborPathList.map((vp) =>\n    buildNoteNode(vp, fileIndex, obsByPath)\n  );\n\n  // -------------------------------------------------------------------------\n  // 6. Deduplicate edges\n  // -------------------------------------------------------------------------\n\n  const edgeKeys = new Set<string>();\n  const edges: NoteEdge[] = [];\n  for (const e of retainedEdges) {\n    const key = `${e.source}|||${e.target}`;\n    if (!edgeKeys.has(key)) {\n      edgeKeys.add(key);\n      edges.push({\n        source: e.source,\n        target: e.target,\n        type: \"wikilink\",\n        weight: 1.0,\n      });\n    }\n  }\n\n  return {\n    focal,\n    neighbors,\n    edges,\n    cluster_membership: {},\n  };\n}\n"],"mappings":"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"}