@tekmidian/pai 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/{aibroker-client-D5NT0S1N.mjs → aibroker-client-Bu72yPKj.mjs} +1 -1
  2. package/dist/{aibroker-client-Bn9SnGMa.mjs → aibroker-client-CDIXmq-e.mjs} +1 -1
  3. package/dist/{aibroker-client-Bn9SnGMa.mjs.map → aibroker-client-CDIXmq-e.mjs.map} +1 -1
  4. package/dist/{chain-CNSa0A3G.mjs → chain-BBbVbWVv.mjs} +4 -4
  5. package/dist/{chain-CNSa0A3G.mjs.map → chain-BBbVbWVv.mjs.map} +1 -1
  6. package/dist/cli/index.mjs +14 -14
  7. package/dist/cli/program.mjs +14 -14
  8. package/dist/config-Bn9jpblO.mjs +653 -0
  9. package/dist/config-Bn9jpblO.mjs.map +1 -0
  10. package/dist/{config-DxMZjpvY.mjs → config-DYXARgk8.mjs} +180 -151
  11. package/dist/config-DYXARgk8.mjs.map +1 -0
  12. package/dist/config-RjeVGJ7K.mjs +3 -0
  13. package/dist/{context-handover-cache-B9NKSjwR.mjs → context-handover-cache-yOuK9RIk.mjs} +5 -5
  14. package/dist/{context-handover-cache-B9NKSjwR.mjs.map → context-handover-cache-yOuK9RIk.mjs.map} +1 -1
  15. package/dist/daemon/index.mjs +13 -12
  16. package/dist/daemon/index.mjs.map +1 -1
  17. package/dist/{daemon-DZ4Q0jrs.mjs → daemon-CBTQYtHF.mjs} +49 -30
  18. package/dist/daemon-CBTQYtHF.mjs.map +1 -0
  19. package/dist/daemon-Vf7pHnjR.mjs +25 -0
  20. package/dist/daemon-mcp/index.mjs +186 -23
  21. package/dist/daemon-mcp/index.mjs.map +1 -1
  22. package/dist/factory-Ba8j9K7q.mjs +3 -0
  23. package/dist/{factory-DNYAqjH0.mjs → factory-Bt7TWQje.mjs} +3 -3
  24. package/dist/{factory-DNYAqjH0.mjs.map → factory-Bt7TWQje.mjs.map} +1 -1
  25. package/dist/{fallback-IeR2FrFn.mjs → fallback-CIGspCSi.mjs} +260 -10
  26. package/dist/fallback-CIGspCSi.mjs.map +1 -0
  27. package/dist/hooks/capture-all-events.mjs +80 -8
  28. package/dist/hooks/capture-all-events.mjs.map +4 -4
  29. package/dist/hooks/context-compression-hook.mjs +54 -39
  30. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  31. package/dist/hooks/post-compact-inject.mjs +54 -39
  32. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  33. package/dist/hooks/route-agents-to-worker.mjs +204 -109
  34. package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
  35. package/dist/hooks/whisper-rules.mjs +7613 -9
  36. package/dist/hooks/whisper-rules.mjs.map +4 -4
  37. package/dist/hooks/worker-proxy.mjs +204 -109
  38. package/dist/hooks/worker-proxy.mjs.map +4 -4
  39. package/dist/hooks/worker-status-line.mjs +211 -116
  40. package/dist/hooks/worker-status-line.mjs.map +4 -4
  41. package/dist/hooks/worker-supervision.mjs +208 -113
  42. package/dist/hooks/worker-supervision.mjs.map +4 -4
  43. package/dist/{kg-entity-BSjM5xZh.mjs → kg-entity-BSW7ra8i.mjs} +1 -1
  44. package/dist/{kg-entity-BSjM5xZh.mjs.map → kg-entity-BSW7ra8i.mjs.map} +1 -1
  45. package/dist/main-resolver-BTastpp4.mjs +7 -0
  46. package/dist/{main-resolver-DvYu9jbl.mjs → main-resolver-BmZPCmgj.mjs} +3 -3
  47. package/dist/{main-resolver-DvYu9jbl.mjs.map → main-resolver-BmZPCmgj.mjs.map} +1 -1
  48. package/dist/{planner-Cn8HmT7l.mjs → planner-BiH9msqq.mjs} +6 -6
  49. package/dist/{planner-Cn8HmT7l.mjs.map → planner-BiH9msqq.mjs.map} +1 -1
  50. package/dist/{program-Bgv1lFFA.mjs → program-BX67-4I2.mjs} +536 -260
  51. package/dist/program-BX67-4I2.mjs.map +1 -0
  52. package/dist/query-feedback-Bm6XvCmg.mjs +3 -0
  53. package/dist/{query-feedback-CYyOGi3y.mjs → query-feedback-CTs_jx6i.mjs} +1 -1
  54. package/dist/{query-feedback-CYyOGi3y.mjs.map → query-feedback-CTs_jx6i.mjs.map} +1 -1
  55. package/dist/{run-Cls0YGgV.mjs → run-TChOnMO_.mjs} +1388 -1183
  56. package/dist/run-TChOnMO_.mjs.map +1 -0
  57. package/dist/{server-Ch99xt5K.mjs → server-R9UkCn3v.mjs} +42 -3
  58. package/dist/server-R9UkCn3v.mjs.map +1 -0
  59. package/dist/session-keepalive-BuG3wvik.mjs +579 -0
  60. package/dist/session-keepalive-BuG3wvik.mjs.map +1 -0
  61. package/dist/skills/VaultContext/SKILL.md +1 -1
  62. package/dist/{state-DnYGRmS7.mjs → state-Ca0ltLgI.mjs} +1 -1
  63. package/dist/{state-PJrgfDLW.mjs → state-DwrsOoMi.mjs} +1 -1
  64. package/dist/{state-PJrgfDLW.mjs.map → state-DwrsOoMi.mjs.map} +1 -1
  65. package/dist/{tools-B77WHeVZ.mjs → tools-BEqiJt_j.mjs} +13 -13
  66. package/dist/{tools-B77WHeVZ.mjs.map → tools-BEqiJt_j.mjs.map} +1 -1
  67. package/dist/{tools-NnR_n-Ja.mjs → tools-BwsOTmjh.mjs} +2 -2
  68. package/dist/{wakeup-UrhiW-KU.mjs → wakeup-Yk5lQ0jb.mjs} +1 -1
  69. package/dist/{wakeup-UrhiW-KU.mjs.map → wakeup-Yk5lQ0jb.mjs.map} +1 -1
  70. package/dist/{work-queue-worker-BA5IDBwQ.mjs → work-queue-worker-B9zVWImP.mjs} +3 -3
  71. package/dist/{work-queue-worker-BA5IDBwQ.mjs.map → work-queue-worker-B9zVWImP.mjs.map} +1 -1
  72. package/dist/work-queue-worker-D-DnQ7oh.mjs +14 -0
  73. package/dist/{zettelkasten-BBjA7GcP.mjs → zettelkasten-DezbXghl.mjs} +2 -2
  74. package/dist/{zettelkasten-BBjA7GcP.mjs.map → zettelkasten-DezbXghl.mjs.map} +1 -1
  75. package/docs/cache-keepalive.md +138 -0
  76. package/docs/commands/README.md +7 -0
  77. package/docs/commands/audit.md +1 -0
  78. package/docs/commands/config.md +84 -0
  79. package/docs/commands/daemon.md +6 -0
  80. package/docs/commands/worker.md +37 -2
  81. package/docs/config.md +171 -0
  82. package/docs/model-catalogue.md +502 -0
  83. package/docs/provider-independence-details.md +1 -1
  84. package/docs/task-bus.md +1 -1
  85. package/docs/worker.md +33 -5
  86. package/docs/workers-config.md +105 -12
  87. package/package.json +1 -1
  88. package/src/hooks/ts/capture-all-events.test.ts +71 -0
  89. package/src/hooks/ts/capture-all-events.ts +10 -3
  90. package/src/hooks/ts/user-prompt/whisper-rules.test.ts +56 -1
  91. package/src/hooks/ts/user-prompt/whisper-rules.ts +24 -0
  92. package/statusline-command.sh +81 -6
  93. package/dist/config-DCu8THKi.mjs +0 -228
  94. package/dist/config-DCu8THKi.mjs.map +0 -1
  95. package/dist/config-DeqFqa1N.mjs +0 -3
  96. package/dist/config-DxMZjpvY.mjs.map +0 -1
  97. package/dist/daemon-CYpVy_Dw.mjs +0 -24
  98. package/dist/daemon-DZ4Q0jrs.mjs.map +0 -1
  99. package/dist/factory-47Qf_df7.mjs +0 -3
  100. package/dist/fallback-IeR2FrFn.mjs.map +0 -1
  101. package/dist/indexer-C5Le7XE7.mjs +0 -1
  102. package/dist/main-resolver-CnfnWa8C.mjs +0 -7
  103. package/dist/program-Bgv1lFFA.mjs.map +0 -1
  104. package/dist/query-feedback-C7rSfiXU.mjs +0 -3
  105. package/dist/run-Cls0YGgV.mjs.map +0 -1
  106. package/dist/server-Ch99xt5K.mjs.map +0 -1
  107. package/dist/work-queue-worker-DLM2wlqM.mjs +0 -14
@@ -1 +1 @@
1
- {"version":3,"file":"zettelkasten-BBjA7GcP.mjs","names":["MAX_CHUNKS"],"sources":["../src/zettelkasten/explore.ts","../src/zettelkasten/surprise.ts","../src/zettelkasten/converse.ts","../src/zettelkasten/health.ts","../src/zettelkasten/suggest.ts","../src/zettelkasten/god-notes.ts","../src/zettelkasten/communities.ts"],"sourcesContent":["import type { StorageBackend } from \"../storage/interface.js\";\nimport { dirname } from \"node:path\";\n\nexport interface ExploreOptions {\n startNote: string;\n depth?: number;\n direction?: \"forward\" | \"backward\" | \"both\";\n mode?: \"sequential\" | \"associative\" | \"all\";\n}\n\nexport interface ExploreNode {\n path: string;\n title: string | null;\n depth: number;\n linkType: \"sequential\" | \"associative\";\n inbound: number;\n outbound: number;\n}\n\nexport interface ExploreResult {\n root: string;\n nodes: ExploreNode[];\n edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }>;\n branchingPoints: string[];\n maxDepthReached: boolean;\n}\n\nfunction classifyEdge(source: string, target: string): \"sequential\" | \"associative\" {\n return dirname(source) === dirname(target) ? \"sequential\" : \"associative\";\n}\n\nasync function resolveStart(backend: StorageBackend, startNote: string): Promise<string | null> {\n // Try direct lookup first\n const files = await backend.getVaultFilesByPaths([startNote]);\n if (files.length > 0) return files[0].vaultPath;\n\n // Try alias lookup\n const alias = await backend.getVaultAlias(startNote);\n if (!alias) return null;\n\n const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);\n return canonical.length > 0 ? canonical[0].vaultPath : null;\n}\n\nasync function getForwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksFromSource(path);\n return links.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n}\n\nasync function getBackwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksToTarget(path);\n return links.map(l => l.sourcePath);\n}\n\nasync function getFileInfo(\n backend: StorageBackend,\n path: string,\n): Promise<{ title: string | null; inbound: number; outbound: number }> {\n const [files, health] = await Promise.all([\n backend.getVaultFilesByPaths([path]),\n backend.getVaultHealth(path),\n ]);\n\n return {\n title: files[0]?.title ?? null,\n inbound: health?.inboundCount ?? 0,\n outbound: health?.outboundCount ?? 0,\n };\n}\n\n/**\n * Traverse the Zettelkasten link graph using BFS, following chains of thought\n * from a starting note up to a configurable depth.\n */\nexport async function zettelExplore(backend: StorageBackend, opts: ExploreOptions): Promise<ExploreResult> {\n const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);\n const direction = opts.direction ?? \"both\";\n const mode = opts.mode ?? \"all\";\n\n const root = await resolveStart(backend, opts.startNote);\n if (!root) {\n return {\n root: opts.startNote,\n nodes: [],\n edges: [],\n branchingPoints: [],\n maxDepthReached: false,\n };\n }\n\n const visited = new Set<string>([root]);\n const nodes: ExploreNode[] = [];\n const edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }> = [];\n let maxDepthReached = false;\n\n const queue: Array<{ path: string; depth: number }> = [{ path: root, depth: 0 }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n if (current.depth >= depth) {\n maxDepthReached = true;\n continue;\n }\n\n const neighbors: Array<{ neighbor: string; from: string; to: string }> = [];\n\n if (direction === \"forward\" || direction === \"both\") {\n for (const n of await getForwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: current.path, to: n });\n }\n }\n\n if (direction === \"backward\" || direction === \"both\") {\n for (const n of await getBackwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: n, to: current.path });\n }\n }\n\n for (const { neighbor, from, to } of neighbors) {\n const edgeType = classifyEdge(from, to);\n\n if (mode !== \"all\" && edgeType !== mode) {\n continue;\n }\n\n const alreadyHasEdge = edges.some((e) => e.from === from && e.to === to);\n if (!alreadyHasEdge) {\n edges.push({ from, to, type: edgeType });\n }\n\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n\n const info = await getFileInfo(backend, neighbor);\n nodes.push({\n path: neighbor,\n title: info.title,\n depth: current.depth + 1,\n linkType: edgeType,\n inbound: info.inbound,\n outbound: info.outbound,\n });\n\n queue.push({ path: neighbor, depth: current.depth + 1 });\n }\n }\n }\n\n const branchingPoints = nodes\n .filter((n) => n.outbound > 2)\n .map((n) => n.path);\n\n const rootInfo = await getFileInfo(backend, root);\n if (rootInfo.outbound > 2) {\n branchingPoints.unshift(root);\n }\n\n return { root, nodes, edges, branchingPoints, maxDepthReached };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport {\n deserializeEmbedding,\n generateEmbedding,\n cosineSimilarity,\n} from \"../memory/embeddings.js\";\n\nexport interface SurpriseOptions {\n referencePath: string;\n vaultProjectId: number;\n limit?: number;\n minSimilarity?: number;\n minGraphDistance?: number;\n}\n\nexport interface SurpriseResult {\n path: string;\n title: string | null;\n cosineSimilarity: number;\n graphDistance: number;\n surpriseScore: number;\n sharedSnippet: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst BFS_HOP_CAP = 20;\n\nasync function getFileEmbeddings(\n backend: StorageBackend,\n projectId: number,\n): Promise<Map<string, { embedding: Float32Array; text: string }>> {\n const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number; text: string }>();\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1, text: row.text });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const result = new Map<string, { embedding: Float32Array; text: string }>();\n for (const [path, { sum, count, text }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n result.set(path, { embedding: avg, text });\n }\n return result;\n}\n\nasync function getReferenceEmbedding(\n backend: StorageBackend,\n projectId: number,\n path: string,\n): Promise<{ embedding: Float32Array; found: boolean }> {\n const rows = await backend.getChunksForPath(projectId, path);\n\n if (rows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const embRows = rows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const dim = deserializeEmbedding(embRows[0].embedding).length;\n const sum = new Float32Array(dim);\n for (const row of embRows) {\n const vec = deserializeEmbedding(row.embedding);\n for (let i = 0; i < dim; i++) {\n sum[i] += vec[i];\n }\n }\n const avg = new Float32Array(dim);\n for (let i = 0; i < dim; i++) {\n avg[i] = sum[i] / embRows.length;\n }\n return { embedding: avg, found: true };\n}\n\nasync function bfsGraphDistance(backend: StorageBackend, source: string, target: string): Promise<number> {\n if (source === target) return 0;\n\n const visited = new Set<string>([source]);\n const queue: Array<{ path: string; hops: number }> = [{ path: source, hops: 0 }];\n\n while (queue.length > 0) {\n const { path, hops } = queue.shift()!;\n if (hops >= BFS_HOP_CAP) continue;\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getLinksFromSource(path),\n backend.getLinksToTarget(path),\n ]);\n\n const neighbors: string[] = [\n ...forwardLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string),\n ...backwardLinks.map(l => l.sourcePath),\n ];\n\n for (const neighbor of neighbors) {\n if (neighbor === target) return hops + 1;\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n queue.push({ path: neighbor, hops: hops + 1 });\n }\n }\n }\n\n return Infinity;\n}\n\nfunction getBestChunkText(\n chunkRows: Array<{ text: string; embedding: Buffer | null }>,\n refEmbedding: Float32Array,\n): string {\n const rows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (rows.length === 0) return \"\";\n\n let bestText = rows[0].text;\n let bestSim = -Infinity;\n\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const sim = cosineSimilarity(refEmbedding, vec);\n if (sim > bestSim) {\n bestSim = sim;\n bestText = row.text;\n }\n }\n\n return bestText.trim().slice(0, 200);\n}\n\n/**\n * Find notes that are semantically similar to a reference note but graph-distant —\n * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.\n */\nexport async function zettelSurprise(\n backend: StorageBackend,\n opts: SurpriseOptions,\n): Promise<SurpriseResult[]> {\n const limit = opts.limit ?? 10;\n const minSimilarity = opts.minSimilarity ?? 0.3;\n const minGraphDistance = opts.minGraphDistance ?? 3;\n\n let { embedding: refEmbedding, found } = await getReferenceEmbedding(\n backend,\n opts.vaultProjectId,\n opts.referencePath,\n );\n\n // Fall back to generating an embedding from the file title if no chunks exist\n if (!found) {\n const files = await backend.getVaultFilesByPaths([opts.referencePath]);\n const text = files[0]?.title ?? opts.referencePath;\n refEmbedding = await generateEmbedding(text, true);\n }\n\n const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);\n\n // Remove the reference note itself from candidates\n allFileEmbeddings.delete(opts.referencePath);\n\n // First pass: filter by semantic similarity to avoid BFS on all nodes\n const semanticCandidates: Array<{ path: string; sim: number }> = [];\n for (const [path, { embedding }] of allFileEmbeddings) {\n const sim = cosineSimilarity(refEmbedding, embedding);\n if (sim >= minSimilarity) {\n semanticCandidates.push({ path, sim });\n }\n }\n\n // Compute graph distances for semantic candidates\n const results: SurpriseResult[] = [];\n\n for (const { path, sim } of semanticCandidates) {\n const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);\n\n const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;\n if (effectiveDistance < minGraphDistance) continue;\n\n const files = await backend.getVaultFilesByPaths([path]);\n const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);\n\n const surpriseScore = sim * Math.log2(effectiveDistance + 1);\n const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);\n\n results.push({\n path,\n title: files[0]?.title ?? null,\n cosineSimilarity: sim,\n graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,\n surpriseScore,\n sharedSnippet,\n });\n }\n\n results.sort((a, b) => b.surpriseScore - a.surpriseScore);\n return results.slice(0, limit);\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport type { SearchResult } from \"../memory/search.js\";\nimport { generateEmbedding } from \"../memory/embeddings.js\";\n\nexport interface ConverseOptions {\n /** The user's question or topic to explore. */\n question: string;\n /** project_id for vault chunks in memory_chunks. */\n vaultProjectId: number;\n /** Graph expansion depth. Default 2. */\n depth?: number;\n /** Maximum number of relevant notes to return. Default 15. */\n limit?: number;\n}\n\nexport interface ConverseConnection {\n fromPath: string;\n toPath: string;\n /** Top-level folder of fromPath. */\n fromDomain: string;\n /** Top-level folder of toPath. */\n toDomain: string;\n /** Link count between these two notes (can be > 1). */\n strength: number;\n}\n\nexport interface ConverseResult {\n relevantNotes: Array<{\n path: string;\n title: string | null;\n snippet: string;\n score: number;\n domain: string;\n }>;\n /** Cross-domain connections found among the selected notes. */\n connections: ConverseConnection[];\n /** Unique domains involved across all selected notes. */\n domains: string[];\n /** AI-ready prompt combining notes + connections for insight generation. */\n synthesisPrompt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract the top-level folder from a vault path (first path segment). */\nfunction extractDomain(vaultPath: string): string {\n const slash = vaultPath.indexOf(\"/\");\n return slash === -1 ? vaultPath : vaultPath.slice(0, slash);\n}\n\n/**\n * Expand one level of graph neighbors for a set of paths.\n * Returns all outbound and inbound neighbor paths (excluding already-visited).\n */\nasync function expandNeighbors(backend: StorageBackend, paths: Set<string>): Promise<string[]> {\n if (paths.size === 0) return [];\n const pathList = Array.from(paths);\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getVaultLinksFromPaths(pathList),\n Promise.all(pathList.map(p => backend.getLinksToTarget(p))),\n ]);\n\n const neighbors: string[] = [];\n for (const link of forwardLinks) {\n if (link.targetPath) neighbors.push(link.targetPath);\n }\n for (const linkList of backwardLinks) {\n for (const link of linkList) {\n neighbors.push(link.sourcePath);\n }\n }\n return neighbors;\n}\n\n/**\n * Hybrid search combining keyword + semantic results using the StorageBackend.\n */\nasync function hybridSearch(\n backend: StorageBackend,\n query: string,\n queryEmbedding: Float32Array,\n opts: { projectIds?: number[]; maxResults?: number },\n): Promise<SearchResult[]> {\n const maxResults = opts.maxResults ?? 10;\n const kw = 0.5;\n const sw = 0.5;\n\n const [keywordResults, semanticResults] = await Promise.all([\n backend.searchKeyword(query, { ...opts, maxResults: 50 }),\n backend.searchSemantic(queryEmbedding, { ...opts, maxResults: 50 }),\n ]);\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(scores: number[]): number[] {\n const min = Math.min(...scores);\n const max = Math.max(...scores);\n const range = max - min;\n if (range === 0) return scores.map(() => 1.0);\n return scores.map(s => (s - min) / range);\n }\n\n const kwNorm = minMaxNormalize(keywordResults.map(r => r.score));\n const semNorm = minMaxNormalize(semanticResults.map(r => r.score));\n\n const combined = new Map<string, SearchResult & { combinedScore: number }>();\n\n for (let i = 0; i < keywordResults.length; i++) {\n const r = keywordResults[i];\n const k = keyFor(r);\n combined.set(k, { ...r, combinedScore: kw * kwNorm[i] });\n }\n\n for (let i = 0; i < semanticResults.length; i++) {\n const r = semanticResults[i];\n const k = keyFor(r);\n const existing = combined.get(k);\n if (existing) {\n existing.combinedScore += sw * semNorm[i];\n } else {\n combined.set(k, { ...r, combinedScore: sw * semNorm[i] });\n }\n }\n\n const sorted = Array.from(combined.values())\n .sort((a, b) => b.combinedScore - a.combinedScore)\n .slice(0, maxResults);\n\n return sorted.map(r => ({ ...r, score: r.combinedScore }));\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Let the vault \"talk back\" — find notes relevant to a question, expand\n * through the link graph, identify cross-domain connections, and return a\n * structured result including a synthesis prompt for an AI to generate insights.\n */\nexport async function zettelConverse(\n backend: StorageBackend,\n opts: ConverseOptions,\n): Promise<ConverseResult> {\n const depth = Math.max(opts.depth ?? 2, 0);\n const limit = Math.max(opts.limit ?? 15, 1);\n const candidateLimit = 20;\n\n // ------------------------------------------------------------------\n // 1. Hybrid search: find top candidates via BM25 + semantic similarity\n // ------------------------------------------------------------------\n const queryEmbedding = await generateEmbedding(opts.question, true);\n\n const searchResults = await hybridSearch(\n backend,\n opts.question,\n queryEmbedding,\n {\n projectIds: [opts.vaultProjectId],\n maxResults: candidateLimit,\n },\n );\n\n // Map of path -> best score + snippet from search results\n const searchHits = new Map<string, { score: number; snippet: string }>();\n for (const r of searchResults) {\n const existing = searchHits.get(r.path);\n if (!existing || r.score > existing.score) {\n searchHits.set(r.path, { score: r.score, snippet: r.snippet });\n }\n }\n\n // ------------------------------------------------------------------\n // 2. Graph expansion: BFS from each search result up to `depth` levels\n // ------------------------------------------------------------------\n const allPaths = new Set<string>(searchHits.keys());\n let frontier = new Set<string>(searchHits.keys());\n\n for (let d = 0; d < depth; d++) {\n const neighbors = await expandNeighbors(backend, frontier);\n const newFrontier = new Set<string>();\n for (const n of neighbors) {\n if (!allPaths.has(n)) {\n allPaths.add(n);\n newFrontier.add(n);\n }\n }\n if (newFrontier.size === 0) break;\n frontier = newFrontier;\n }\n\n // ------------------------------------------------------------------\n // 3. Deduplicate + trim to limit\n // ------------------------------------------------------------------\n const searchRanked = Array.from(searchHits.entries())\n .sort((a, b) => b[1].score - a[1].score)\n .map(([path, info]) => ({ path, ...info, isSearchResult: true }));\n\n const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));\n\n // Fetch health data for neighbor ranking\n const neighborHealthRows = await Promise.all(\n neighborPaths.map(p => backend.getVaultHealth(p))\n );\n const neighborRanked = neighborPaths\n .map((path, idx) => ({\n path,\n score: 0,\n snippet: \"\",\n inbound: neighborHealthRows[idx]?.inboundCount ?? 0,\n isSearchResult: false,\n }))\n .sort((a, b) => b.inbound - a.inbound);\n\n const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);\n const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);\n\n const selectedSearchPaths = searchRanked.slice(0, limit);\n const selectedPaths = new Set<string>([\n ...selectedSearchPaths.map((r) => r.path),\n ...selectedNeighbors.map((r) => r.path),\n ]);\n\n // ------------------------------------------------------------------\n // 4. Build relevantNotes with titles + domains\n // ------------------------------------------------------------------\n\n // Fetch titles in bulk\n const allSelectedPaths = Array.from(selectedPaths);\n const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);\n const titleMap = new Map<string, string | null>(fileRows.map(f => [f.vaultPath, f.title]));\n\n const relevantNotes: ConverseResult[\"relevantNotes\"] = [];\n\n for (const r of selectedSearchPaths) {\n if (!selectedPaths.has(r.path)) continue;\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: r.score,\n domain: extractDomain(r.path),\n });\n }\n\n for (const r of selectedNeighbors) {\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: 0,\n domain: extractDomain(r.path),\n });\n }\n\n // ------------------------------------------------------------------\n // 5. Find connections between the selected notes\n // ------------------------------------------------------------------\n let connections: ConverseConnection[] = [];\n\n if (selectedPaths.size > 0) {\n const pathList = Array.from(selectedPaths);\n const pathSet = new Set(pathList);\n\n // Get all outbound links from the selected paths\n const linkRows = await backend.getVaultLinksFromPaths(pathList);\n\n // Count links between selected paths\n const edgeCounts = new Map<string, number>();\n for (const link of linkRows) {\n if (link.targetPath && pathSet.has(link.targetPath)) {\n const key = `${link.sourcePath}|||${link.targetPath}`;\n edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);\n }\n }\n\n for (const [key, cnt] of edgeCounts) {\n const [sourcePath, targetPath] = key.split(\"|||\");\n connections.push({\n fromPath: sourcePath,\n toPath: targetPath,\n fromDomain: extractDomain(sourcePath),\n toDomain: extractDomain(targetPath),\n strength: cnt,\n });\n }\n }\n\n // ------------------------------------------------------------------\n // 6. Domains + cross-domain filter\n // ------------------------------------------------------------------\n const domainSet = new Set<string>(relevantNotes.map((n) => n.domain));\n const domains = Array.from(domainSet).sort();\n\n const crossDomainConnections = connections.filter(\n (c) => c.fromDomain !== c.toDomain,\n );\n\n // ------------------------------------------------------------------\n // 7. Build synthesis prompt\n // ------------------------------------------------------------------\n const notesSummary = relevantNotes\n .map((n, i) => {\n const title = n.title ? `\"${n.title}\"` : \"(untitled)\";\n const domain = n.domain;\n const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : \" [context]\";\n const snippet = n.snippet.trim().slice(0, 300);\n return `${i + 1}. [${domain}] ${title}${scoreLabel}\\n Path: ${n.path}\\n \"${snippet}\"`;\n })\n .join(\"\\n\\n\");\n\n const connectionSummary =\n crossDomainConnections.length > 0\n ? crossDomainConnections\n .map(\n (c) =>\n `- \"${c.fromPath}\" (${c.fromDomain}) → \"${c.toPath}\" (${c.toDomain}) [strength: ${c.strength}]`,\n )\n .join(\"\\n\")\n : \"(no cross-domain connections found)\";\n\n const domainList = domains.join(\", \");\n\n const synthesisPrompt = `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:\n\nQUESTION: ${opts.question}\n\n---\n\nRELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):\n\n${notesSummary}\n\n---\n\nCROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):\n\n${connectionSummary}\n\n---\n\nSYNTHESIS TASK:\n\nBased on these notes and the connections between them, please:\n\n1. Identify the key insights that emerge in direct response to the question.\n2. Highlight any unexpected connections between notes from different domains (${domainList}).\n3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.\n4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.\n5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.\n\nThink like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`;\n\n return {\n relevantNotes,\n connections: crossDomainConnections,\n domains,\n synthesisPrompt,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface HealthOptions {\n scope?: \"full\" | \"recent\" | \"project\";\n projectPath?: string;\n recentDays?: number;\n include?: Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\">;\n}\n\nexport interface DeadLink {\n sourcePath: string;\n targetRaw: string;\n lineNumber: number;\n}\n\nexport interface HealthResult {\n totalFiles: number;\n totalLinks: number;\n deadLinks: DeadLink[];\n orphans: string[];\n disconnectedClusters: number;\n lowConnectivity: string[];\n healthScore: number;\n computedAt: number;\n /** Breakdown of links by confidence level (if confidence tagging is active). */\n linkConfidence?: {\n extracted: number;\n inferred: number;\n ambiguous: number;\n };\n}\n\nfunction countComponents(nodes: string[], edges: Array<{ source: string; target: string }>): number {\n if (nodes.length === 0) return 0;\n\n const parent = new Map<string, string>();\n const rank = new Map<string, number>();\n\n for (const n of nodes) {\n parent.set(n, n);\n rank.set(n, 0);\n }\n\n function find(x: string): string {\n let root = x;\n while (parent.get(root) !== root) {\n root = parent.get(root)!;\n }\n let current = x;\n while (current !== root) {\n const next = parent.get(current)!;\n parent.set(current, root);\n current = next;\n }\n return root;\n }\n\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra === rb) return;\n const rankA = rank.get(ra) ?? 0;\n const rankB = rank.get(rb) ?? 0;\n if (rankA < rankB) {\n parent.set(ra, rb);\n } else if (rankA > rankB) {\n parent.set(rb, ra);\n } else {\n parent.set(rb, ra);\n rank.set(ra, rankA + 1);\n }\n }\n\n for (const { source, target } of edges) {\n if (parent.has(source) && parent.has(target)) {\n union(source, target);\n }\n }\n\n const roots = new Set<string>();\n for (const n of nodes) {\n roots.add(find(n));\n }\n return roots.size;\n}\n\n/**\n * Audit the structural health of the Zettelkasten vault using graph metrics.\n */\nexport async function zettelHealth(backend: StorageBackend, opts?: HealthOptions): Promise<HealthResult> {\n const options = opts ?? {};\n const scope = options.scope ?? \"full\";\n const include = options.include ?? [\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"];\n\n const computedAt = Date.now();\n\n // --- totalFiles ---\n let totalFiles = 0;\n if (scope === \"full\") {\n totalFiles = await backend.countVaultFiles();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalFiles = await backend.countVaultFilesWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalFiles = await backend.countVaultFilesAfter(cutoff);\n }\n\n // --- totalLinks ---\n let totalLinks = 0;\n if (scope === \"full\") {\n // Count total links via link graph length\n const graph = await backend.getVaultLinkGraph();\n totalLinks = graph.length;\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalLinks = await backend.countVaultLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalLinks = await backend.countVaultLinksAfter(cutoff);\n }\n\n // --- deadLinks ---\n let deadLinks: DeadLink[] = [];\n if (include.includes(\"dead_links\")) {\n if (scope === \"full\") {\n deadLinks = await backend.getDeadLinksWithLineNumbers();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n deadLinks = await backend.getDeadLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n deadLinks = await backend.getDeadLinksAfter(cutoff);\n }\n }\n\n // --- orphans ---\n let orphans: string[] = [];\n if (include.includes(\"orphans\")) {\n if (scope === \"full\") {\n const orphanRows = await backend.getOrphans();\n orphans = orphanRows.map(r => r.vaultPath);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n orphans = await backend.getOrphansWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n orphans = await backend.getOrphansAfter(cutoff);\n }\n }\n\n // --- disconnectedClusters (union-find) ---\n let disconnectedClusters = 1;\n if (include.includes(\"disconnected\")) {\n let allNodes: string[];\n let allEdges: Array<{ source: string; target: string }>;\n\n if (scope === \"full\") {\n [allNodes, allEdges] = await Promise.all([\n backend.getAllVaultFilePaths(),\n backend.getVaultLinkEdges(),\n ]);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsWithPrefix(prefix),\n backend.getVaultLinkEdgesWithPrefix(prefix),\n ]);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsAfter(cutoff),\n backend.getVaultLinkEdgesAfter(cutoff),\n ]);\n }\n\n disconnectedClusters = countComponents(allNodes, allEdges);\n }\n\n // --- lowConnectivity ---\n let lowConnectivity: string[] = [];\n if (include.includes(\"low_connectivity\")) {\n if (scope === \"full\") {\n lowConnectivity = await backend.getLowConnectivity();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n lowConnectivity = await backend.getLowConnectivityAfter(cutoff);\n }\n }\n\n // --- linkConfidence ---\n // Count links by confidence level using a sampling of all vault file paths.\n // We reuse the link graph data we already fetched for totalLinks when scope=full.\n let linkConfidence: HealthResult[\"linkConfidence\"] | undefined;\n if (scope === \"full\") {\n try {\n // Get all file paths to iterate their outbound links with confidence\n const samplePaths = await backend.getAllVaultFilePaths();\n const sampleSize = Math.min(samplePaths.length, 200); // limit for performance\n const sampled = samplePaths.slice(0, sampleSize);\n let extracted = 0;\n let inferred = 0;\n let ambiguous = 0;\n for (const path of sampled) {\n const links = await backend.getLinksFromSource(path);\n for (const link of links) {\n const c = link.confidence ?? \"EXTRACTED\";\n if (c === \"EXTRACTED\") extracted++;\n else if (c === \"INFERRED\") inferred++;\n else ambiguous++;\n }\n }\n // Extrapolate if we sampled\n const factor = samplePaths.length > 0 ? samplePaths.length / sampleSize : 1;\n linkConfidence = {\n extracted: Math.round(extracted * factor),\n inferred: Math.round(inferred * factor),\n ambiguous: Math.round(ambiguous * factor),\n };\n } catch {\n // Backend may not support confidence field yet — ignore\n }\n }\n\n // --- healthScore ---\n const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;\n const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;\n const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;\n const healthScore = Math.round(\n 100 * (1 - deadRatio) * (1 - orphanRatio * 0.5) * (1 - lowConnRatio * 0.3),\n );\n\n return {\n totalFiles,\n totalLinks,\n deadLinks,\n orphans,\n disconnectedClusters,\n lowConnectivity,\n healthScore,\n computedAt,\n linkConfidence,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../memory/embeddings.js\";\nimport { basename } from \"node:path\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\nexport interface SuggestOptions {\n notePath: string;\n vaultProjectId: number;\n limit?: number;\n excludeLinked?: boolean;\n}\n\nexport interface Suggestion {\n path: string;\n title: string | null;\n score: number;\n semanticScore: number;\n tagScore: number;\n neighborScore: number;\n reason: string;\n suggestedWikilink: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst SEMANTIC_WEIGHT = 0.5;\nconst TAG_WEIGHT = 0.2;\nconst NEIGHBOR_WEIGHT = 0.3;\n\n// STOP_WORDS imported from utils/stop-words.ts\n\nfunction extractTagsFromChunkTexts(texts: string[]): Set<string> {\n const tags = new Set<string>();\n for (const text of texts) {\n // Match YAML frontmatter tags block: \"tags:\\n - tag1\\n - tag2\"\n const match = text.match(/^tags:\\s*\\n((?:[ \\t]*-[ \\t]*.+\\n?)*)/m);\n if (!match) continue;\n const block = match[1];\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n const tagMatch = line.match(/^[ \\t]*-[ \\t]*(.+)/);\n if (tagMatch) {\n const tag = tagMatch[1].trim().toLowerCase();\n if (tag) tags.add(tag);\n }\n }\n }\n return tags;\n}\n\nfunction jaccardSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const tag of a) {\n if (b.has(tag)) intersection++;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction buildReason(\n semanticScore: number,\n tagScore: number,\n neighborScore: number,\n neighborCount: number,\n): string {\n const signals: Array<{ label: string; value: number }> = [\n { label: `Semantically similar (${semanticScore.toFixed(2)})`, value: semanticScore * SEMANTIC_WEIGHT },\n { label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`, value: tagScore * TAG_WEIGHT },\n { label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? \"s\" : \"\"}`, value: neighborScore * NEIGHBOR_WEIGHT },\n ];\n signals.sort((a, b) => b.value - a.value);\n return signals[0].label;\n}\n\nfunction suggestedWikilink(vaultPath: string): string {\n const base = basename(vaultPath);\n const name = base.endsWith(\".md\") ? base.slice(0, -3) : base;\n return `[[${name}]]`;\n}\n\n/**\n * Proactively find notes worth linking to a given note, combining semantic similarity,\n * shared tags, and graph-neighborhood signals into a ranked list of suggestions.\n */\nexport async function zettelSuggest(\n backend: StorageBackend,\n opts: SuggestOptions,\n): Promise<Suggestion[]> {\n const limit = opts.limit ?? 5;\n const excludeLinked = opts.excludeLinked ?? true;\n\n // Step 1: get current outbound links\n const outboundLinks = await backend.getLinksFromSource(opts.notePath);\n const linkedPaths = new Set(outboundLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string));\n\n // Step 2a: get all file-level embeddings for semantic scoring\n const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number }>();\n for (const row of chunkRows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1 });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const allEmbeddings = new Map<string, Float32Array>();\n for (const [path, { sum, count }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n allEmbeddings.set(path, avg);\n }\n allEmbeddings.delete(opts.notePath);\n\n // Step 2b: get source embedding\n const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;\n\n // Step 2c: get source tags\n const sourceChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5);\n const sourceTags = extractTagsFromChunkTexts(sourceChunkTexts.map(r => r.text));\n\n // Step 2d: compute graph neighborhood (friends-of-friends)\n const directLinks = await backend.getLinksFromSource(opts.notePath);\n const directTargets = directLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n\n const friendLinkCounts = new Map<string, number>();\n for (const target of directTargets) {\n const friendLinks = await backend.getLinksFromSource(target);\n for (const link of friendLinks) {\n if (link.targetPath && link.targetPath !== opts.notePath) {\n friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);\n }\n }\n }\n const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());\n\n // Get all vault files to enumerate candidates\n const allFiles = await backend.getAllVaultFiles();\n\n const suggestions: Suggestion[] = [];\n\n for (const fileRow of allFiles) {\n const vault_path = fileRow.vaultPath;\n const title = fileRow.title;\n\n if (vault_path === opts.notePath) continue;\n if (excludeLinked && linkedPaths.has(vault_path)) continue;\n\n // Semantic score\n let semanticScore = 0;\n if (sourceEmbedding) {\n const candidateEmbedding = allEmbeddings.get(vault_path);\n if (candidateEmbedding) {\n semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));\n }\n }\n\n // Tag score (only compute if candidate might have chunks)\n let tagScore = 0;\n if (allEmbeddings.has(vault_path)) {\n const candidateChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5);\n const candidateTags = extractTagsFromChunkTexts(candidateChunkTexts.map(r => r.text));\n tagScore = jaccardSimilarity(sourceTags, candidateTags);\n }\n\n // Neighbor score\n const friendCount = friendLinkCounts.get(vault_path) ?? 0;\n const neighborScore = friendCount / maxFriendLinks;\n\n const score =\n SEMANTIC_WEIGHT * semanticScore +\n TAG_WEIGHT * tagScore +\n NEIGHBOR_WEIGHT * neighborScore;\n\n // Only include if there is at least some signal\n if (score <= 0) continue;\n\n const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);\n\n suggestions.push({\n path: vault_path,\n title,\n score,\n semanticScore,\n tagScore,\n neighborScore,\n reason,\n suggestedWikilink: suggestedWikilink(vault_path),\n });\n }\n\n suggestions.sort((a, b) => b.score - a.score);\n return suggestions.slice(0, limit);\n}\n","/**\n * God-note detection — find hub notes via degree centrality on the vault link graph.\n *\n * Hub notes (or \"god notes\") are notes with unusually high inbound link counts.\n * They represent core concepts that the rest of the vault frequently references.\n * Structural pages (index, MOC, master notes, tag pages) are filtered out.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface GodNoteOptions {\n /** Maximum number of hub notes to return. Default: 20. */\n limit?: number;\n /** Minimum inbound link count to qualify as a hub. Default: 3. */\n minInbound?: number;\n /** Include outbound counts in the result. Default: true. */\n includeOutbound?: boolean;\n}\n\nexport interface GodNote {\n path: string;\n title: string | null;\n inboundCount: number;\n outboundCount: number;\n /** Ratio of inbound to total degree — higher means more \"sink\"-like. */\n inboundRatio: number;\n}\n\nexport interface GodNoteResult {\n godNotes: GodNote[];\n totalVaultFiles: number;\n /** Median inbound count across all vault files (for context). */\n medianInbound: number;\n}\n\n/** Patterns that identify structural/meta pages rather than concept notes. */\nconst STRUCTURAL_PATTERNS = [\n /\\bindex\\b/i,\n /\\bMOC\\b/,\n /\\bmaster\\b/i,\n /\\btag\\s*page/i,\n /\\bhome\\b/i,\n /\\bdashboard\\b/i,\n /\\btemplate/i,\n /^_/,\n];\n\nfunction isStructuralNote(title: string | null, path: string): boolean {\n const text = title ?? path;\n return STRUCTURAL_PATTERNS.some((pattern) => pattern.test(text));\n}\n\n/**\n * Find hub/\"god\" notes in the vault — notes with the highest inbound link counts,\n * excluding structural pages.\n */\nexport async function zettelGodNotes(\n backend: StorageBackend,\n opts?: GodNoteOptions,\n): Promise<GodNoteResult> {\n const limit = opts?.limit ?? 20;\n const minInbound = opts?.minInbound ?? 3;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n\n // Count inbound and outbound per path\n const inboundCounts = new Map<string, number>();\n const outboundCounts = new Map<string, number>();\n\n for (const { source_path, target_path } of linkGraph) {\n inboundCounts.set(target_path, (inboundCounts.get(target_path) ?? 0) + 1);\n outboundCounts.set(source_path, (outboundCounts.get(source_path) ?? 0) + 1);\n }\n\n // Get all vault files for title lookup and total count\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n const totalVaultFiles = allFiles.length;\n\n // Compute median inbound for context\n const allInbounds = allFiles.map((f) => inboundCounts.get(f.vaultPath) ?? 0);\n allInbounds.sort((a, b) => a - b);\n const medianInbound =\n allInbounds.length > 0\n ? allInbounds[Math.floor(allInbounds.length / 2)]\n : 0;\n\n // Build candidate list: all paths with inbound >= minInbound, excluding structural\n const candidates: GodNote[] = [];\n\n for (const [path, inbound] of inboundCounts) {\n if (inbound < minInbound) continue;\n\n const title = titleMap.get(path) ?? null;\n if (isStructuralNote(title, path)) continue;\n\n const outbound = outboundCounts.get(path) ?? 0;\n const totalDegree = inbound + outbound;\n const inboundRatio = totalDegree > 0 ? inbound / totalDegree : 0;\n\n candidates.push({\n path,\n title,\n inboundCount: inbound,\n outboundCount: outbound,\n inboundRatio: Math.round(inboundRatio * 1000) / 1000,\n });\n }\n\n // Sort by inbound count descending\n candidates.sort((a, b) => b.inboundCount - a.inboundCount);\n\n return {\n godNotes: candidates.slice(0, limit),\n totalVaultFiles,\n medianInbound,\n };\n}\n","/**\n * Community detection for the Zettelkasten vault link graph.\n *\n * Implements the Louvain algorithm in pure TypeScript for discovering\n * densely connected clusters of notes. This reveals emergent knowledge\n * communities that may not be obvious from folder structure alone.\n *\n * The Louvain method optimizes modularity in two phases:\n * 1. Local: each node greedily moves to the community that maximizes modularity gain.\n * 2. Aggregation: communities are collapsed into super-nodes, forming a new graph.\n * Repeat until no further improvement.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface CommunityOptions {\n /** Minimum community size to include in results. Default: 3. */\n minSize?: number;\n /** Maximum number of communities to return. Default: 20. */\n maxCommunities?: number;\n /** Resolution parameter for Louvain (higher = more communities). Default: 1.0. */\n resolution?: number;\n}\n\nexport interface CommunityNode {\n path: string;\n title: string | null;\n /** Degree (inbound + outbound) within the community. */\n internalDegree: number;\n}\n\nexport interface Community {\n id: number;\n /** Label derived from most common words in note titles. */\n label: string;\n nodes: CommunityNode[];\n size: number;\n /** Cohesion: ratio of internal edges to total possible internal edges. */\n cohesion: number;\n /** Top folders represented in this community. */\n topFolders: string[];\n}\n\nexport interface CommunityResult {\n communities: Community[];\n totalNodes: number;\n totalEdges: number;\n /** Global modularity score of the partition. */\n modularity: number;\n}\n\n// ---------------------------------------------------------------------------\n// Louvain algorithm implementation\n// ---------------------------------------------------------------------------\n\ninterface Graph {\n /** All node IDs */\n nodes: string[];\n /** Adjacency list: node -> Map<neighbor, weight> */\n adj: Map<string, Map<string, number>>;\n /** Total edge weight (sum of all edge weights, counting undirected edges once) */\n totalWeight: number;\n /** Weighted degree per node */\n degree: Map<string, number>;\n}\n\nfunction buildUndirectedGraph(\n edges: Array<{ source_path: string; target_path: string }>\n): Graph {\n const adj = new Map<string, Map<string, number>>();\n const nodeSet = new Set<string>();\n\n function getOrCreate(node: string): Map<string, number> {\n let m = adj.get(node);\n if (!m) {\n m = new Map();\n adj.set(node, m);\n }\n nodeSet.add(node);\n return m;\n }\n\n let totalWeight = 0;\n\n for (const { source_path, target_path } of edges) {\n if (source_path === target_path) continue; // skip self-loops\n\n const aMap = getOrCreate(source_path);\n const bMap = getOrCreate(target_path);\n\n // Undirected: add weight in both directions\n aMap.set(target_path, (aMap.get(target_path) ?? 0) + 1);\n bMap.set(source_path, (bMap.get(source_path) ?? 0) + 1);\n totalWeight += 1; // each undirected edge counted once\n }\n\n // Compute weighted degree\n const degree = new Map<string, number>();\n for (const [node, neighbors] of adj) {\n let d = 0;\n for (const w of neighbors.values()) d += w;\n degree.set(node, d);\n }\n\n return {\n nodes: Array.from(nodeSet),\n adj,\n totalWeight,\n degree,\n };\n}\n\n/**\n * Run one pass of Phase 1: local node movement.\n * Returns true if any node changed community.\n */\nfunction louvainPhase1(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): boolean {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return false;\n\n // Community -> sum of degrees of nodes in community\n const communityDegreeSum = new Map<number, number>();\n // Community -> sum of internal edge weights\n const communityInternalWeight = new Map<number, number>();\n\n for (const node of graph.nodes) {\n const c = community.get(node)!;\n communityDegreeSum.set(c, (communityDegreeSum.get(c) ?? 0) + (graph.degree.get(node) ?? 0));\n }\n\n // Compute internal weights\n for (const [node, neighbors] of graph.adj) {\n const nc = community.get(node)!;\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === nc) {\n communityInternalWeight.set(nc, (communityInternalWeight.get(nc) ?? 0) + weight);\n }\n }\n }\n // Each internal edge is counted twice (both endpoints), so divide\n for (const [c, w] of communityInternalWeight) {\n communityInternalWeight.set(c, w / 2);\n }\n\n let improved = false;\n // Shuffle node order for better convergence\n const shuffled = [...graph.nodes];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];\n }\n\n for (const node of shuffled) {\n const currentComm = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n const neighbors = graph.adj.get(node) ?? new Map();\n\n // Compute weight to each neighboring community\n const weightToComm = new Map<number, number>();\n for (const [neighbor, weight] of neighbors) {\n const nc = community.get(neighbor)!;\n weightToComm.set(nc, (weightToComm.get(nc) ?? 0) + weight);\n }\n\n // Remove node from its current community\n communityDegreeSum.set(currentComm, (communityDegreeSum.get(currentComm) ?? 0) - ki);\n const weightToCurrent = weightToComm.get(currentComm) ?? 0;\n communityInternalWeight.set(\n currentComm,\n (communityInternalWeight.get(currentComm) ?? 0) - weightToCurrent,\n );\n\n // Find best community\n let bestComm = currentComm;\n let bestDelta = 0;\n\n for (const [candidateComm, weightToCandidate] of weightToComm) {\n const sigmaTot = communityDegreeSum.get(candidateComm) ?? 0;\n // Modularity gain of moving node to candidateComm\n const delta = weightToCandidate - resolution * (ki * sigmaTot) / m2;\n if (delta > bestDelta) {\n bestDelta = delta;\n bestComm = candidateComm;\n }\n }\n\n // Also consider staying (delta = 0, but need to re-add)\n // Move node to best community\n community.set(node, bestComm);\n communityDegreeSum.set(bestComm, (communityDegreeSum.get(bestComm) ?? 0) + ki);\n const weightToBest = weightToComm.get(bestComm) ?? 0;\n communityInternalWeight.set(\n bestComm,\n (communityInternalWeight.get(bestComm) ?? 0) + weightToBest,\n );\n\n if (bestComm !== currentComm) {\n improved = true;\n }\n }\n\n return improved;\n}\n\n/**\n * Compute modularity for a given partition.\n */\nfunction computeModularity(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): number {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return 0;\n\n let q = 0;\n for (const [node, neighbors] of graph.adj) {\n const ci = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === ci) {\n q += weight - resolution * (ki * (graph.degree.get(neighbor) ?? 0)) / m2;\n }\n }\n }\n return q / m2;\n}\n\n/**\n * Run Louvain community detection on the vault link graph.\n */\nfunction runLouvain(\n graph: Graph,\n resolution: number,\n): Map<string, number> {\n // Initialize: each node in its own community\n const community = new Map<string, number>();\n let nextComm = 0;\n for (const node of graph.nodes) {\n community.set(node, nextComm++);\n }\n\n // Phase 1: iteratively move nodes until no improvement\n const MAX_ITERATIONS = 20;\n for (let iter = 0; iter < MAX_ITERATIONS; iter++) {\n const improved = louvainPhase1(graph, community, resolution);\n if (!improved) break;\n }\n\n // Compact community IDs\n const commRemap = new Map<number, number>();\n let remapIdx = 0;\n for (const [, c] of community) {\n if (!commRemap.has(c)) {\n commRemap.set(c, remapIdx++);\n }\n }\n for (const [node, c] of community) {\n community.set(node, commRemap.get(c)!);\n }\n\n return community;\n}\n\n// ---------------------------------------------------------------------------\n// Label generation\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"the\", \"and\", \"for\", \"are\", \"but\", \"not\", \"you\", \"all\", \"can\", \"her\",\n \"was\", \"one\", \"our\", \"out\", \"has\", \"had\", \"how\", \"its\", \"may\", \"new\",\n \"now\", \"old\", \"see\", \"way\", \"who\", \"did\", \"get\", \"let\", \"say\", \"she\",\n \"too\", \"use\", \"from\", \"with\", \"this\", \"that\", \"will\", \"been\", \"have\",\n \"each\", \"make\", \"like\", \"long\", \"look\", \"many\", \"them\", \"then\", \"what\",\n \"when\", \"some\", \"time\", \"very\", \"your\", \"about\", \"could\", \"into\", \"just\",\n \"more\", \"note\", \"notes\", \"than\", \"over\",\n]);\n\nfunction generateCommunityLabel(titles: Array<string | null>): string {\n const wordCounts = new Map<string, number>();\n for (const title of titles) {\n if (!title) continue;\n const words = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted\n .slice(0, 3)\n .map(([w]) => w)\n .join(\" / \") || \"unnamed\";\n}\n\nfunction getTopFolder(path: string): string {\n const slash = path.indexOf(\"/\");\n return slash === -1 ? path : path.slice(0, slash);\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Detect communities in the vault link graph using the Louvain algorithm.\n * Returns clusters of densely connected notes with labels and cohesion scores.\n */\nexport async function zettelCommunities(\n backend: StorageBackend,\n opts?: CommunityOptions,\n): Promise<CommunityResult> {\n const minSize = opts?.minSize ?? 3;\n const maxCommunities = opts?.maxCommunities ?? 20;\n const resolution = opts?.resolution ?? 1.0;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n const graph = buildUndirectedGraph(linkGraph);\n\n if (graph.nodes.length === 0) {\n return {\n communities: [],\n totalNodes: 0,\n totalEdges: 0,\n modularity: 0,\n };\n }\n\n // Run Louvain\n const communityMap = runLouvain(graph, resolution);\n\n // Compute modularity\n const modularity = computeModularity(graph, communityMap, resolution);\n\n // Group nodes by community\n const groups = new Map<number, string[]>();\n for (const [node, comm] of communityMap) {\n const arr = groups.get(comm);\n if (arr) arr.push(node);\n else groups.set(comm, [node]);\n }\n\n // Get all vault files for title lookup\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n // Build community results\n const communities: Community[] = [];\n let commId = 0;\n\n for (const [, members] of groups) {\n if (members.length < minSize) continue;\n\n const memberSet = new Set(members);\n const titles = members.map((p) => titleMap.get(p) ?? null);\n const label = generateCommunityLabel(titles);\n\n // Compute internal degree per node\n const nodes: CommunityNode[] = members.map((path) => {\n const neighbors = graph.adj.get(path) ?? new Map();\n let internalDeg = 0;\n for (const [neighbor, weight] of neighbors) {\n if (memberSet.has(neighbor)) internalDeg += weight;\n }\n return {\n path,\n title: titleMap.get(path) ?? null,\n internalDegree: internalDeg,\n };\n });\n\n // Sort nodes by internal degree descending\n nodes.sort((a, b) => b.internalDegree - a.internalDegree);\n\n // Compute cohesion: ratio of actual internal edges to possible\n let internalEdges = 0;\n for (const node of nodes) {\n internalEdges += node.internalDegree;\n }\n internalEdges /= 2; // each edge counted twice\n const possibleEdges = (members.length * (members.length - 1)) / 2;\n const cohesion = possibleEdges > 0 ? Math.round((internalEdges / possibleEdges) * 1000) / 1000 : 0;\n\n // Top folders\n const folderCounts = new Map<string, number>();\n for (const path of members) {\n const folder = getTopFolder(path);\n folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);\n }\n const topFolders = [...folderCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([f]) => f);\n\n communities.push({\n id: commId++,\n label,\n nodes,\n size: members.length,\n cohesion,\n topFolders,\n });\n }\n\n // Sort by size descending\n communities.sort((a, b) => b.size - a.size);\n\n return {\n communities: communities.slice(0, maxCommunities),\n totalNodes: graph.nodes.length,\n totalEdges: graph.totalWeight,\n modularity: Math.round(modularity * 10000) / 10000,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"zettelkasten-DezbXghl.mjs","names":["MAX_CHUNKS"],"sources":["../src/zettelkasten/explore.ts","../src/zettelkasten/surprise.ts","../src/zettelkasten/converse.ts","../src/zettelkasten/health.ts","../src/zettelkasten/suggest.ts","../src/zettelkasten/god-notes.ts","../src/zettelkasten/communities.ts"],"sourcesContent":["import type { StorageBackend } from \"../storage/interface.js\";\nimport { dirname } from \"node:path\";\n\nexport interface ExploreOptions {\n startNote: string;\n depth?: number;\n direction?: \"forward\" | \"backward\" | \"both\";\n mode?: \"sequential\" | \"associative\" | \"all\";\n}\n\nexport interface ExploreNode {\n path: string;\n title: string | null;\n depth: number;\n linkType: \"sequential\" | \"associative\";\n inbound: number;\n outbound: number;\n}\n\nexport interface ExploreResult {\n root: string;\n nodes: ExploreNode[];\n edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }>;\n branchingPoints: string[];\n maxDepthReached: boolean;\n}\n\nfunction classifyEdge(source: string, target: string): \"sequential\" | \"associative\" {\n return dirname(source) === dirname(target) ? \"sequential\" : \"associative\";\n}\n\nasync function resolveStart(backend: StorageBackend, startNote: string): Promise<string | null> {\n // Try direct lookup first\n const files = await backend.getVaultFilesByPaths([startNote]);\n if (files.length > 0) return files[0].vaultPath;\n\n // Try alias lookup\n const alias = await backend.getVaultAlias(startNote);\n if (!alias) return null;\n\n const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);\n return canonical.length > 0 ? canonical[0].vaultPath : null;\n}\n\nasync function getForwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksFromSource(path);\n return links.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n}\n\nasync function getBackwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksToTarget(path);\n return links.map(l => l.sourcePath);\n}\n\nasync function getFileInfo(\n backend: StorageBackend,\n path: string,\n): Promise<{ title: string | null; inbound: number; outbound: number }> {\n const [files, health] = await Promise.all([\n backend.getVaultFilesByPaths([path]),\n backend.getVaultHealth(path),\n ]);\n\n return {\n title: files[0]?.title ?? null,\n inbound: health?.inboundCount ?? 0,\n outbound: health?.outboundCount ?? 0,\n };\n}\n\n/**\n * Traverse the Zettelkasten link graph using BFS, following chains of thought\n * from a starting note up to a configurable depth.\n */\nexport async function zettelExplore(backend: StorageBackend, opts: ExploreOptions): Promise<ExploreResult> {\n const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);\n const direction = opts.direction ?? \"both\";\n const mode = opts.mode ?? \"all\";\n\n const root = await resolveStart(backend, opts.startNote);\n if (!root) {\n return {\n root: opts.startNote,\n nodes: [],\n edges: [],\n branchingPoints: [],\n maxDepthReached: false,\n };\n }\n\n const visited = new Set<string>([root]);\n const nodes: ExploreNode[] = [];\n const edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }> = [];\n let maxDepthReached = false;\n\n const queue: Array<{ path: string; depth: number }> = [{ path: root, depth: 0 }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n if (current.depth >= depth) {\n maxDepthReached = true;\n continue;\n }\n\n const neighbors: Array<{ neighbor: string; from: string; to: string }> = [];\n\n if (direction === \"forward\" || direction === \"both\") {\n for (const n of await getForwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: current.path, to: n });\n }\n }\n\n if (direction === \"backward\" || direction === \"both\") {\n for (const n of await getBackwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: n, to: current.path });\n }\n }\n\n for (const { neighbor, from, to } of neighbors) {\n const edgeType = classifyEdge(from, to);\n\n if (mode !== \"all\" && edgeType !== mode) {\n continue;\n }\n\n const alreadyHasEdge = edges.some((e) => e.from === from && e.to === to);\n if (!alreadyHasEdge) {\n edges.push({ from, to, type: edgeType });\n }\n\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n\n const info = await getFileInfo(backend, neighbor);\n nodes.push({\n path: neighbor,\n title: info.title,\n depth: current.depth + 1,\n linkType: edgeType,\n inbound: info.inbound,\n outbound: info.outbound,\n });\n\n queue.push({ path: neighbor, depth: current.depth + 1 });\n }\n }\n }\n\n const branchingPoints = nodes\n .filter((n) => n.outbound > 2)\n .map((n) => n.path);\n\n const rootInfo = await getFileInfo(backend, root);\n if (rootInfo.outbound > 2) {\n branchingPoints.unshift(root);\n }\n\n return { root, nodes, edges, branchingPoints, maxDepthReached };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport {\n deserializeEmbedding,\n generateEmbedding,\n cosineSimilarity,\n} from \"../memory/embeddings.js\";\n\nexport interface SurpriseOptions {\n referencePath: string;\n vaultProjectId: number;\n limit?: number;\n minSimilarity?: number;\n minGraphDistance?: number;\n}\n\nexport interface SurpriseResult {\n path: string;\n title: string | null;\n cosineSimilarity: number;\n graphDistance: number;\n surpriseScore: number;\n sharedSnippet: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst BFS_HOP_CAP = 20;\n\nasync function getFileEmbeddings(\n backend: StorageBackend,\n projectId: number,\n): Promise<Map<string, { embedding: Float32Array; text: string }>> {\n const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number; text: string }>();\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1, text: row.text });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const result = new Map<string, { embedding: Float32Array; text: string }>();\n for (const [path, { sum, count, text }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n result.set(path, { embedding: avg, text });\n }\n return result;\n}\n\nasync function getReferenceEmbedding(\n backend: StorageBackend,\n projectId: number,\n path: string,\n): Promise<{ embedding: Float32Array; found: boolean }> {\n const rows = await backend.getChunksForPath(projectId, path);\n\n if (rows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const embRows = rows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const dim = deserializeEmbedding(embRows[0].embedding).length;\n const sum = new Float32Array(dim);\n for (const row of embRows) {\n const vec = deserializeEmbedding(row.embedding);\n for (let i = 0; i < dim; i++) {\n sum[i] += vec[i];\n }\n }\n const avg = new Float32Array(dim);\n for (let i = 0; i < dim; i++) {\n avg[i] = sum[i] / embRows.length;\n }\n return { embedding: avg, found: true };\n}\n\nasync function bfsGraphDistance(backend: StorageBackend, source: string, target: string): Promise<number> {\n if (source === target) return 0;\n\n const visited = new Set<string>([source]);\n const queue: Array<{ path: string; hops: number }> = [{ path: source, hops: 0 }];\n\n while (queue.length > 0) {\n const { path, hops } = queue.shift()!;\n if (hops >= BFS_HOP_CAP) continue;\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getLinksFromSource(path),\n backend.getLinksToTarget(path),\n ]);\n\n const neighbors: string[] = [\n ...forwardLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string),\n ...backwardLinks.map(l => l.sourcePath),\n ];\n\n for (const neighbor of neighbors) {\n if (neighbor === target) return hops + 1;\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n queue.push({ path: neighbor, hops: hops + 1 });\n }\n }\n }\n\n return Infinity;\n}\n\nfunction getBestChunkText(\n chunkRows: Array<{ text: string; embedding: Buffer | null }>,\n refEmbedding: Float32Array,\n): string {\n const rows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (rows.length === 0) return \"\";\n\n let bestText = rows[0].text;\n let bestSim = -Infinity;\n\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const sim = cosineSimilarity(refEmbedding, vec);\n if (sim > bestSim) {\n bestSim = sim;\n bestText = row.text;\n }\n }\n\n return bestText.trim().slice(0, 200);\n}\n\n/**\n * Find notes that are semantically similar to a reference note but graph-distant —\n * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.\n */\nexport async function zettelSurprise(\n backend: StorageBackend,\n opts: SurpriseOptions,\n): Promise<SurpriseResult[]> {\n const limit = opts.limit ?? 10;\n const minSimilarity = opts.minSimilarity ?? 0.3;\n const minGraphDistance = opts.minGraphDistance ?? 3;\n\n let { embedding: refEmbedding, found } = await getReferenceEmbedding(\n backend,\n opts.vaultProjectId,\n opts.referencePath,\n );\n\n // Fall back to generating an embedding from the file title if no chunks exist\n if (!found) {\n const files = await backend.getVaultFilesByPaths([opts.referencePath]);\n const text = files[0]?.title ?? opts.referencePath;\n refEmbedding = await generateEmbedding(text, true);\n }\n\n const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);\n\n // Remove the reference note itself from candidates\n allFileEmbeddings.delete(opts.referencePath);\n\n // First pass: filter by semantic similarity to avoid BFS on all nodes\n const semanticCandidates: Array<{ path: string; sim: number }> = [];\n for (const [path, { embedding }] of allFileEmbeddings) {\n const sim = cosineSimilarity(refEmbedding, embedding);\n if (sim >= minSimilarity) {\n semanticCandidates.push({ path, sim });\n }\n }\n\n // Compute graph distances for semantic candidates\n const results: SurpriseResult[] = [];\n\n for (const { path, sim } of semanticCandidates) {\n const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);\n\n const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;\n if (effectiveDistance < minGraphDistance) continue;\n\n const files = await backend.getVaultFilesByPaths([path]);\n const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);\n\n const surpriseScore = sim * Math.log2(effectiveDistance + 1);\n const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);\n\n results.push({\n path,\n title: files[0]?.title ?? null,\n cosineSimilarity: sim,\n graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,\n surpriseScore,\n sharedSnippet,\n });\n }\n\n results.sort((a, b) => b.surpriseScore - a.surpriseScore);\n return results.slice(0, limit);\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport type { SearchResult } from \"../memory/search.js\";\nimport { generateEmbedding } from \"../memory/embeddings.js\";\n\nexport interface ConverseOptions {\n /** The user's question or topic to explore. */\n question: string;\n /** project_id for vault chunks in memory_chunks. */\n vaultProjectId: number;\n /** Graph expansion depth. Default 2. */\n depth?: number;\n /** Maximum number of relevant notes to return. Default 15. */\n limit?: number;\n}\n\nexport interface ConverseConnection {\n fromPath: string;\n toPath: string;\n /** Top-level folder of fromPath. */\n fromDomain: string;\n /** Top-level folder of toPath. */\n toDomain: string;\n /** Link count between these two notes (can be > 1). */\n strength: number;\n}\n\nexport interface ConverseResult {\n relevantNotes: Array<{\n path: string;\n title: string | null;\n snippet: string;\n score: number;\n domain: string;\n }>;\n /** Cross-domain connections found among the selected notes. */\n connections: ConverseConnection[];\n /** Unique domains involved across all selected notes. */\n domains: string[];\n /** AI-ready prompt combining notes + connections for insight generation. */\n synthesisPrompt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract the top-level folder from a vault path (first path segment). */\nfunction extractDomain(vaultPath: string): string {\n const slash = vaultPath.indexOf(\"/\");\n return slash === -1 ? vaultPath : vaultPath.slice(0, slash);\n}\n\n/**\n * Expand one level of graph neighbors for a set of paths.\n * Returns all outbound and inbound neighbor paths (excluding already-visited).\n */\nasync function expandNeighbors(backend: StorageBackend, paths: Set<string>): Promise<string[]> {\n if (paths.size === 0) return [];\n const pathList = Array.from(paths);\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getVaultLinksFromPaths(pathList),\n Promise.all(pathList.map(p => backend.getLinksToTarget(p))),\n ]);\n\n const neighbors: string[] = [];\n for (const link of forwardLinks) {\n if (link.targetPath) neighbors.push(link.targetPath);\n }\n for (const linkList of backwardLinks) {\n for (const link of linkList) {\n neighbors.push(link.sourcePath);\n }\n }\n return neighbors;\n}\n\n/**\n * Hybrid search combining keyword + semantic results using the StorageBackend.\n */\nasync function hybridSearch(\n backend: StorageBackend,\n query: string,\n queryEmbedding: Float32Array,\n opts: { projectIds?: number[]; maxResults?: number },\n): Promise<SearchResult[]> {\n const maxResults = opts.maxResults ?? 10;\n const kw = 0.5;\n const sw = 0.5;\n\n const [keywordResults, semanticResults] = await Promise.all([\n backend.searchKeyword(query, { ...opts, maxResults: 50 }),\n backend.searchSemantic(queryEmbedding, { ...opts, maxResults: 50 }),\n ]);\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(scores: number[]): number[] {\n const min = Math.min(...scores);\n const max = Math.max(...scores);\n const range = max - min;\n if (range === 0) return scores.map(() => 1.0);\n return scores.map(s => (s - min) / range);\n }\n\n const kwNorm = minMaxNormalize(keywordResults.map(r => r.score));\n const semNorm = minMaxNormalize(semanticResults.map(r => r.score));\n\n const combined = new Map<string, SearchResult & { combinedScore: number }>();\n\n for (let i = 0; i < keywordResults.length; i++) {\n const r = keywordResults[i];\n const k = keyFor(r);\n combined.set(k, { ...r, combinedScore: kw * kwNorm[i] });\n }\n\n for (let i = 0; i < semanticResults.length; i++) {\n const r = semanticResults[i];\n const k = keyFor(r);\n const existing = combined.get(k);\n if (existing) {\n existing.combinedScore += sw * semNorm[i];\n } else {\n combined.set(k, { ...r, combinedScore: sw * semNorm[i] });\n }\n }\n\n const sorted = Array.from(combined.values())\n .sort((a, b) => b.combinedScore - a.combinedScore)\n .slice(0, maxResults);\n\n return sorted.map(r => ({ ...r, score: r.combinedScore }));\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Let the vault \"talk back\" — find notes relevant to a question, expand\n * through the link graph, identify cross-domain connections, and return a\n * structured result including a synthesis prompt for an AI to generate insights.\n */\nexport async function zettelConverse(\n backend: StorageBackend,\n opts: ConverseOptions,\n): Promise<ConverseResult> {\n const depth = Math.max(opts.depth ?? 2, 0);\n const limit = Math.max(opts.limit ?? 15, 1);\n const candidateLimit = 20;\n\n // ------------------------------------------------------------------\n // 1. Hybrid search: find top candidates via BM25 + semantic similarity\n // ------------------------------------------------------------------\n const queryEmbedding = await generateEmbedding(opts.question, true);\n\n const searchResults = await hybridSearch(\n backend,\n opts.question,\n queryEmbedding,\n {\n projectIds: [opts.vaultProjectId],\n maxResults: candidateLimit,\n },\n );\n\n // Map of path -> best score + snippet from search results\n const searchHits = new Map<string, { score: number; snippet: string }>();\n for (const r of searchResults) {\n const existing = searchHits.get(r.path);\n if (!existing || r.score > existing.score) {\n searchHits.set(r.path, { score: r.score, snippet: r.snippet });\n }\n }\n\n // ------------------------------------------------------------------\n // 2. Graph expansion: BFS from each search result up to `depth` levels\n // ------------------------------------------------------------------\n const allPaths = new Set<string>(searchHits.keys());\n let frontier = new Set<string>(searchHits.keys());\n\n for (let d = 0; d < depth; d++) {\n const neighbors = await expandNeighbors(backend, frontier);\n const newFrontier = new Set<string>();\n for (const n of neighbors) {\n if (!allPaths.has(n)) {\n allPaths.add(n);\n newFrontier.add(n);\n }\n }\n if (newFrontier.size === 0) break;\n frontier = newFrontier;\n }\n\n // ------------------------------------------------------------------\n // 3. Deduplicate + trim to limit\n // ------------------------------------------------------------------\n const searchRanked = Array.from(searchHits.entries())\n .sort((a, b) => b[1].score - a[1].score)\n .map(([path, info]) => ({ path, ...info, isSearchResult: true }));\n\n const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));\n\n // Fetch health data for neighbor ranking\n const neighborHealthRows = await Promise.all(\n neighborPaths.map(p => backend.getVaultHealth(p))\n );\n const neighborRanked = neighborPaths\n .map((path, idx) => ({\n path,\n score: 0,\n snippet: \"\",\n inbound: neighborHealthRows[idx]?.inboundCount ?? 0,\n isSearchResult: false,\n }))\n .sort((a, b) => b.inbound - a.inbound);\n\n const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);\n const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);\n\n const selectedSearchPaths = searchRanked.slice(0, limit);\n const selectedPaths = new Set<string>([\n ...selectedSearchPaths.map((r) => r.path),\n ...selectedNeighbors.map((r) => r.path),\n ]);\n\n // ------------------------------------------------------------------\n // 4. Build relevantNotes with titles + domains\n // ------------------------------------------------------------------\n\n // Fetch titles in bulk\n const allSelectedPaths = Array.from(selectedPaths);\n const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);\n const titleMap = new Map<string, string | null>(fileRows.map(f => [f.vaultPath, f.title]));\n\n const relevantNotes: ConverseResult[\"relevantNotes\"] = [];\n\n for (const r of selectedSearchPaths) {\n if (!selectedPaths.has(r.path)) continue;\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: r.score,\n domain: extractDomain(r.path),\n });\n }\n\n for (const r of selectedNeighbors) {\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: 0,\n domain: extractDomain(r.path),\n });\n }\n\n // ------------------------------------------------------------------\n // 5. Find connections between the selected notes\n // ------------------------------------------------------------------\n let connections: ConverseConnection[] = [];\n\n if (selectedPaths.size > 0) {\n const pathList = Array.from(selectedPaths);\n const pathSet = new Set(pathList);\n\n // Get all outbound links from the selected paths\n const linkRows = await backend.getVaultLinksFromPaths(pathList);\n\n // Count links between selected paths\n const edgeCounts = new Map<string, number>();\n for (const link of linkRows) {\n if (link.targetPath && pathSet.has(link.targetPath)) {\n const key = `${link.sourcePath}|||${link.targetPath}`;\n edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);\n }\n }\n\n for (const [key, cnt] of edgeCounts) {\n const [sourcePath, targetPath] = key.split(\"|||\");\n connections.push({\n fromPath: sourcePath,\n toPath: targetPath,\n fromDomain: extractDomain(sourcePath),\n toDomain: extractDomain(targetPath),\n strength: cnt,\n });\n }\n }\n\n // ------------------------------------------------------------------\n // 6. Domains + cross-domain filter\n // ------------------------------------------------------------------\n const domainSet = new Set<string>(relevantNotes.map((n) => n.domain));\n const domains = Array.from(domainSet).sort();\n\n const crossDomainConnections = connections.filter(\n (c) => c.fromDomain !== c.toDomain,\n );\n\n // ------------------------------------------------------------------\n // 7. Build synthesis prompt\n // ------------------------------------------------------------------\n const notesSummary = relevantNotes\n .map((n, i) => {\n const title = n.title ? `\"${n.title}\"` : \"(untitled)\";\n const domain = n.domain;\n const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : \" [context]\";\n const snippet = n.snippet.trim().slice(0, 300);\n return `${i + 1}. [${domain}] ${title}${scoreLabel}\\n Path: ${n.path}\\n \"${snippet}\"`;\n })\n .join(\"\\n\\n\");\n\n const connectionSummary =\n crossDomainConnections.length > 0\n ? crossDomainConnections\n .map(\n (c) =>\n `- \"${c.fromPath}\" (${c.fromDomain}) → \"${c.toPath}\" (${c.toDomain}) [strength: ${c.strength}]`,\n )\n .join(\"\\n\")\n : \"(no cross-domain connections found)\";\n\n const domainList = domains.join(\", \");\n\n const synthesisPrompt = `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:\n\nQUESTION: ${opts.question}\n\n---\n\nRELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):\n\n${notesSummary}\n\n---\n\nCROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):\n\n${connectionSummary}\n\n---\n\nSYNTHESIS TASK:\n\nBased on these notes and the connections between them, please:\n\n1. Identify the key insights that emerge in direct response to the question.\n2. Highlight any unexpected connections between notes from different domains (${domainList}).\n3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.\n4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.\n5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.\n\nThink like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`;\n\n return {\n relevantNotes,\n connections: crossDomainConnections,\n domains,\n synthesisPrompt,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface HealthOptions {\n scope?: \"full\" | \"recent\" | \"project\";\n projectPath?: string;\n recentDays?: number;\n include?: Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\">;\n}\n\nexport interface DeadLink {\n sourcePath: string;\n targetRaw: string;\n lineNumber: number;\n}\n\nexport interface HealthResult {\n totalFiles: number;\n totalLinks: number;\n deadLinks: DeadLink[];\n orphans: string[];\n disconnectedClusters: number;\n lowConnectivity: string[];\n healthScore: number;\n computedAt: number;\n /** Breakdown of links by confidence level (if confidence tagging is active). */\n linkConfidence?: {\n extracted: number;\n inferred: number;\n ambiguous: number;\n };\n}\n\nfunction countComponents(nodes: string[], edges: Array<{ source: string; target: string }>): number {\n if (nodes.length === 0) return 0;\n\n const parent = new Map<string, string>();\n const rank = new Map<string, number>();\n\n for (const n of nodes) {\n parent.set(n, n);\n rank.set(n, 0);\n }\n\n function find(x: string): string {\n let root = x;\n while (parent.get(root) !== root) {\n root = parent.get(root)!;\n }\n let current = x;\n while (current !== root) {\n const next = parent.get(current)!;\n parent.set(current, root);\n current = next;\n }\n return root;\n }\n\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra === rb) return;\n const rankA = rank.get(ra) ?? 0;\n const rankB = rank.get(rb) ?? 0;\n if (rankA < rankB) {\n parent.set(ra, rb);\n } else if (rankA > rankB) {\n parent.set(rb, ra);\n } else {\n parent.set(rb, ra);\n rank.set(ra, rankA + 1);\n }\n }\n\n for (const { source, target } of edges) {\n if (parent.has(source) && parent.has(target)) {\n union(source, target);\n }\n }\n\n const roots = new Set<string>();\n for (const n of nodes) {\n roots.add(find(n));\n }\n return roots.size;\n}\n\n/**\n * Audit the structural health of the Zettelkasten vault using graph metrics.\n */\nexport async function zettelHealth(backend: StorageBackend, opts?: HealthOptions): Promise<HealthResult> {\n const options = opts ?? {};\n const scope = options.scope ?? \"full\";\n const include = options.include ?? [\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"];\n\n const computedAt = Date.now();\n\n // --- totalFiles ---\n let totalFiles = 0;\n if (scope === \"full\") {\n totalFiles = await backend.countVaultFiles();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalFiles = await backend.countVaultFilesWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalFiles = await backend.countVaultFilesAfter(cutoff);\n }\n\n // --- totalLinks ---\n let totalLinks = 0;\n if (scope === \"full\") {\n // Count total links via link graph length\n const graph = await backend.getVaultLinkGraph();\n totalLinks = graph.length;\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalLinks = await backend.countVaultLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalLinks = await backend.countVaultLinksAfter(cutoff);\n }\n\n // --- deadLinks ---\n let deadLinks: DeadLink[] = [];\n if (include.includes(\"dead_links\")) {\n if (scope === \"full\") {\n deadLinks = await backend.getDeadLinksWithLineNumbers();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n deadLinks = await backend.getDeadLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n deadLinks = await backend.getDeadLinksAfter(cutoff);\n }\n }\n\n // --- orphans ---\n let orphans: string[] = [];\n if (include.includes(\"orphans\")) {\n if (scope === \"full\") {\n const orphanRows = await backend.getOrphans();\n orphans = orphanRows.map(r => r.vaultPath);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n orphans = await backend.getOrphansWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n orphans = await backend.getOrphansAfter(cutoff);\n }\n }\n\n // --- disconnectedClusters (union-find) ---\n let disconnectedClusters = 1;\n if (include.includes(\"disconnected\")) {\n let allNodes: string[];\n let allEdges: Array<{ source: string; target: string }>;\n\n if (scope === \"full\") {\n [allNodes, allEdges] = await Promise.all([\n backend.getAllVaultFilePaths(),\n backend.getVaultLinkEdges(),\n ]);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsWithPrefix(prefix),\n backend.getVaultLinkEdgesWithPrefix(prefix),\n ]);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsAfter(cutoff),\n backend.getVaultLinkEdgesAfter(cutoff),\n ]);\n }\n\n disconnectedClusters = countComponents(allNodes, allEdges);\n }\n\n // --- lowConnectivity ---\n let lowConnectivity: string[] = [];\n if (include.includes(\"low_connectivity\")) {\n if (scope === \"full\") {\n lowConnectivity = await backend.getLowConnectivity();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n lowConnectivity = await backend.getLowConnectivityAfter(cutoff);\n }\n }\n\n // --- linkConfidence ---\n // Count links by confidence level using a sampling of all vault file paths.\n // We reuse the link graph data we already fetched for totalLinks when scope=full.\n let linkConfidence: HealthResult[\"linkConfidence\"] | undefined;\n if (scope === \"full\") {\n try {\n // Get all file paths to iterate their outbound links with confidence\n const samplePaths = await backend.getAllVaultFilePaths();\n const sampleSize = Math.min(samplePaths.length, 200); // limit for performance\n const sampled = samplePaths.slice(0, sampleSize);\n let extracted = 0;\n let inferred = 0;\n let ambiguous = 0;\n for (const path of sampled) {\n const links = await backend.getLinksFromSource(path);\n for (const link of links) {\n const c = link.confidence ?? \"EXTRACTED\";\n if (c === \"EXTRACTED\") extracted++;\n else if (c === \"INFERRED\") inferred++;\n else ambiguous++;\n }\n }\n // Extrapolate if we sampled\n const factor = samplePaths.length > 0 ? samplePaths.length / sampleSize : 1;\n linkConfidence = {\n extracted: Math.round(extracted * factor),\n inferred: Math.round(inferred * factor),\n ambiguous: Math.round(ambiguous * factor),\n };\n } catch {\n // Backend may not support confidence field yet — ignore\n }\n }\n\n // --- healthScore ---\n const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;\n const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;\n const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;\n const healthScore = Math.round(\n 100 * (1 - deadRatio) * (1 - orphanRatio * 0.5) * (1 - lowConnRatio * 0.3),\n );\n\n return {\n totalFiles,\n totalLinks,\n deadLinks,\n orphans,\n disconnectedClusters,\n lowConnectivity,\n healthScore,\n computedAt,\n linkConfidence,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../memory/embeddings.js\";\nimport { basename } from \"node:path\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\nexport interface SuggestOptions {\n notePath: string;\n vaultProjectId: number;\n limit?: number;\n excludeLinked?: boolean;\n}\n\nexport interface Suggestion {\n path: string;\n title: string | null;\n score: number;\n semanticScore: number;\n tagScore: number;\n neighborScore: number;\n reason: string;\n suggestedWikilink: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst SEMANTIC_WEIGHT = 0.5;\nconst TAG_WEIGHT = 0.2;\nconst NEIGHBOR_WEIGHT = 0.3;\n\n// STOP_WORDS imported from utils/stop-words.ts\n\nfunction extractTagsFromChunkTexts(texts: string[]): Set<string> {\n const tags = new Set<string>();\n for (const text of texts) {\n // Match YAML frontmatter tags block: \"tags:\\n - tag1\\n - tag2\"\n const match = text.match(/^tags:\\s*\\n((?:[ \\t]*-[ \\t]*.+\\n?)*)/m);\n if (!match) continue;\n const block = match[1];\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n const tagMatch = line.match(/^[ \\t]*-[ \\t]*(.+)/);\n if (tagMatch) {\n const tag = tagMatch[1].trim().toLowerCase();\n if (tag) tags.add(tag);\n }\n }\n }\n return tags;\n}\n\nfunction jaccardSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const tag of a) {\n if (b.has(tag)) intersection++;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction buildReason(\n semanticScore: number,\n tagScore: number,\n neighborScore: number,\n neighborCount: number,\n): string {\n const signals: Array<{ label: string; value: number }> = [\n { label: `Semantically similar (${semanticScore.toFixed(2)})`, value: semanticScore * SEMANTIC_WEIGHT },\n { label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`, value: tagScore * TAG_WEIGHT },\n { label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? \"s\" : \"\"}`, value: neighborScore * NEIGHBOR_WEIGHT },\n ];\n signals.sort((a, b) => b.value - a.value);\n return signals[0].label;\n}\n\nfunction suggestedWikilink(vaultPath: string): string {\n const base = basename(vaultPath);\n const name = base.endsWith(\".md\") ? base.slice(0, -3) : base;\n return `[[${name}]]`;\n}\n\n/**\n * Proactively find notes worth linking to a given note, combining semantic similarity,\n * shared tags, and graph-neighborhood signals into a ranked list of suggestions.\n */\nexport async function zettelSuggest(\n backend: StorageBackend,\n opts: SuggestOptions,\n): Promise<Suggestion[]> {\n const limit = opts.limit ?? 5;\n const excludeLinked = opts.excludeLinked ?? true;\n\n // Step 1: get current outbound links\n const outboundLinks = await backend.getLinksFromSource(opts.notePath);\n const linkedPaths = new Set(outboundLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string));\n\n // Step 2a: get all file-level embeddings for semantic scoring\n const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number }>();\n for (const row of chunkRows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1 });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const allEmbeddings = new Map<string, Float32Array>();\n for (const [path, { sum, count }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n allEmbeddings.set(path, avg);\n }\n allEmbeddings.delete(opts.notePath);\n\n // Step 2b: get source embedding\n const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;\n\n // Step 2c: get source tags\n const sourceChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5);\n const sourceTags = extractTagsFromChunkTexts(sourceChunkTexts.map(r => r.text));\n\n // Step 2d: compute graph neighborhood (friends-of-friends)\n const directLinks = await backend.getLinksFromSource(opts.notePath);\n const directTargets = directLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n\n const friendLinkCounts = new Map<string, number>();\n for (const target of directTargets) {\n const friendLinks = await backend.getLinksFromSource(target);\n for (const link of friendLinks) {\n if (link.targetPath && link.targetPath !== opts.notePath) {\n friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);\n }\n }\n }\n const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());\n\n // Get all vault files to enumerate candidates\n const allFiles = await backend.getAllVaultFiles();\n\n const suggestions: Suggestion[] = [];\n\n for (const fileRow of allFiles) {\n const vault_path = fileRow.vaultPath;\n const title = fileRow.title;\n\n if (vault_path === opts.notePath) continue;\n if (excludeLinked && linkedPaths.has(vault_path)) continue;\n\n // Semantic score\n let semanticScore = 0;\n if (sourceEmbedding) {\n const candidateEmbedding = allEmbeddings.get(vault_path);\n if (candidateEmbedding) {\n semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));\n }\n }\n\n // Tag score (only compute if candidate might have chunks)\n let tagScore = 0;\n if (allEmbeddings.has(vault_path)) {\n const candidateChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5);\n const candidateTags = extractTagsFromChunkTexts(candidateChunkTexts.map(r => r.text));\n tagScore = jaccardSimilarity(sourceTags, candidateTags);\n }\n\n // Neighbor score\n const friendCount = friendLinkCounts.get(vault_path) ?? 0;\n const neighborScore = friendCount / maxFriendLinks;\n\n const score =\n SEMANTIC_WEIGHT * semanticScore +\n TAG_WEIGHT * tagScore +\n NEIGHBOR_WEIGHT * neighborScore;\n\n // Only include if there is at least some signal\n if (score <= 0) continue;\n\n const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);\n\n suggestions.push({\n path: vault_path,\n title,\n score,\n semanticScore,\n tagScore,\n neighborScore,\n reason,\n suggestedWikilink: suggestedWikilink(vault_path),\n });\n }\n\n suggestions.sort((a, b) => b.score - a.score);\n return suggestions.slice(0, limit);\n}\n","/**\n * God-note detection — find hub notes via degree centrality on the vault link graph.\n *\n * Hub notes (or \"god notes\") are notes with unusually high inbound link counts.\n * They represent core concepts that the rest of the vault frequently references.\n * Structural pages (index, MOC, master notes, tag pages) are filtered out.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface GodNoteOptions {\n /** Maximum number of hub notes to return. Default: 20. */\n limit?: number;\n /** Minimum inbound link count to qualify as a hub. Default: 3. */\n minInbound?: number;\n /** Include outbound counts in the result. Default: true. */\n includeOutbound?: boolean;\n}\n\nexport interface GodNote {\n path: string;\n title: string | null;\n inboundCount: number;\n outboundCount: number;\n /** Ratio of inbound to total degree — higher means more \"sink\"-like. */\n inboundRatio: number;\n}\n\nexport interface GodNoteResult {\n godNotes: GodNote[];\n totalVaultFiles: number;\n /** Median inbound count across all vault files (for context). */\n medianInbound: number;\n}\n\n/** Patterns that identify structural/meta pages rather than concept notes. */\nconst STRUCTURAL_PATTERNS = [\n /\\bindex\\b/i,\n /\\bMOC\\b/,\n /\\bmaster\\b/i,\n /\\btag\\s*page/i,\n /\\bhome\\b/i,\n /\\bdashboard\\b/i,\n /\\btemplate/i,\n /^_/,\n];\n\nfunction isStructuralNote(title: string | null, path: string): boolean {\n const text = title ?? path;\n return STRUCTURAL_PATTERNS.some((pattern) => pattern.test(text));\n}\n\n/**\n * Find hub/\"god\" notes in the vault — notes with the highest inbound link counts,\n * excluding structural pages.\n */\nexport async function zettelGodNotes(\n backend: StorageBackend,\n opts?: GodNoteOptions,\n): Promise<GodNoteResult> {\n const limit = opts?.limit ?? 20;\n const minInbound = opts?.minInbound ?? 3;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n\n // Count inbound and outbound per path\n const inboundCounts = new Map<string, number>();\n const outboundCounts = new Map<string, number>();\n\n for (const { source_path, target_path } of linkGraph) {\n inboundCounts.set(target_path, (inboundCounts.get(target_path) ?? 0) + 1);\n outboundCounts.set(source_path, (outboundCounts.get(source_path) ?? 0) + 1);\n }\n\n // Get all vault files for title lookup and total count\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n const totalVaultFiles = allFiles.length;\n\n // Compute median inbound for context\n const allInbounds = allFiles.map((f) => inboundCounts.get(f.vaultPath) ?? 0);\n allInbounds.sort((a, b) => a - b);\n const medianInbound =\n allInbounds.length > 0\n ? allInbounds[Math.floor(allInbounds.length / 2)]\n : 0;\n\n // Build candidate list: all paths with inbound >= minInbound, excluding structural\n const candidates: GodNote[] = [];\n\n for (const [path, inbound] of inboundCounts) {\n if (inbound < minInbound) continue;\n\n const title = titleMap.get(path) ?? null;\n if (isStructuralNote(title, path)) continue;\n\n const outbound = outboundCounts.get(path) ?? 0;\n const totalDegree = inbound + outbound;\n const inboundRatio = totalDegree > 0 ? inbound / totalDegree : 0;\n\n candidates.push({\n path,\n title,\n inboundCount: inbound,\n outboundCount: outbound,\n inboundRatio: Math.round(inboundRatio * 1000) / 1000,\n });\n }\n\n // Sort by inbound count descending\n candidates.sort((a, b) => b.inboundCount - a.inboundCount);\n\n return {\n godNotes: candidates.slice(0, limit),\n totalVaultFiles,\n medianInbound,\n };\n}\n","/**\n * Community detection for the Zettelkasten vault link graph.\n *\n * Implements the Louvain algorithm in pure TypeScript for discovering\n * densely connected clusters of notes. This reveals emergent knowledge\n * communities that may not be obvious from folder structure alone.\n *\n * The Louvain method optimizes modularity in two phases:\n * 1. Local: each node greedily moves to the community that maximizes modularity gain.\n * 2. Aggregation: communities are collapsed into super-nodes, forming a new graph.\n * Repeat until no further improvement.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface CommunityOptions {\n /** Minimum community size to include in results. Default: 3. */\n minSize?: number;\n /** Maximum number of communities to return. Default: 20. */\n maxCommunities?: number;\n /** Resolution parameter for Louvain (higher = more communities). Default: 1.0. */\n resolution?: number;\n}\n\nexport interface CommunityNode {\n path: string;\n title: string | null;\n /** Degree (inbound + outbound) within the community. */\n internalDegree: number;\n}\n\nexport interface Community {\n id: number;\n /** Label derived from most common words in note titles. */\n label: string;\n nodes: CommunityNode[];\n size: number;\n /** Cohesion: ratio of internal edges to total possible internal edges. */\n cohesion: number;\n /** Top folders represented in this community. */\n topFolders: string[];\n}\n\nexport interface CommunityResult {\n communities: Community[];\n totalNodes: number;\n totalEdges: number;\n /** Global modularity score of the partition. */\n modularity: number;\n}\n\n// ---------------------------------------------------------------------------\n// Louvain algorithm implementation\n// ---------------------------------------------------------------------------\n\ninterface Graph {\n /** All node IDs */\n nodes: string[];\n /** Adjacency list: node -> Map<neighbor, weight> */\n adj: Map<string, Map<string, number>>;\n /** Total edge weight (sum of all edge weights, counting undirected edges once) */\n totalWeight: number;\n /** Weighted degree per node */\n degree: Map<string, number>;\n}\n\nfunction buildUndirectedGraph(\n edges: Array<{ source_path: string; target_path: string }>\n): Graph {\n const adj = new Map<string, Map<string, number>>();\n const nodeSet = new Set<string>();\n\n function getOrCreate(node: string): Map<string, number> {\n let m = adj.get(node);\n if (!m) {\n m = new Map();\n adj.set(node, m);\n }\n nodeSet.add(node);\n return m;\n }\n\n let totalWeight = 0;\n\n for (const { source_path, target_path } of edges) {\n if (source_path === target_path) continue; // skip self-loops\n\n const aMap = getOrCreate(source_path);\n const bMap = getOrCreate(target_path);\n\n // Undirected: add weight in both directions\n aMap.set(target_path, (aMap.get(target_path) ?? 0) + 1);\n bMap.set(source_path, (bMap.get(source_path) ?? 0) + 1);\n totalWeight += 1; // each undirected edge counted once\n }\n\n // Compute weighted degree\n const degree = new Map<string, number>();\n for (const [node, neighbors] of adj) {\n let d = 0;\n for (const w of neighbors.values()) d += w;\n degree.set(node, d);\n }\n\n return {\n nodes: Array.from(nodeSet),\n adj,\n totalWeight,\n degree,\n };\n}\n\n/**\n * Run one pass of Phase 1: local node movement.\n * Returns true if any node changed community.\n */\nfunction louvainPhase1(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): boolean {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return false;\n\n // Community -> sum of degrees of nodes in community\n const communityDegreeSum = new Map<number, number>();\n // Community -> sum of internal edge weights\n const communityInternalWeight = new Map<number, number>();\n\n for (const node of graph.nodes) {\n const c = community.get(node)!;\n communityDegreeSum.set(c, (communityDegreeSum.get(c) ?? 0) + (graph.degree.get(node) ?? 0));\n }\n\n // Compute internal weights\n for (const [node, neighbors] of graph.adj) {\n const nc = community.get(node)!;\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === nc) {\n communityInternalWeight.set(nc, (communityInternalWeight.get(nc) ?? 0) + weight);\n }\n }\n }\n // Each internal edge is counted twice (both endpoints), so divide\n for (const [c, w] of communityInternalWeight) {\n communityInternalWeight.set(c, w / 2);\n }\n\n let improved = false;\n // Shuffle node order for better convergence\n const shuffled = [...graph.nodes];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];\n }\n\n for (const node of shuffled) {\n const currentComm = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n const neighbors = graph.adj.get(node) ?? new Map();\n\n // Compute weight to each neighboring community\n const weightToComm = new Map<number, number>();\n for (const [neighbor, weight] of neighbors) {\n const nc = community.get(neighbor)!;\n weightToComm.set(nc, (weightToComm.get(nc) ?? 0) + weight);\n }\n\n // Remove node from its current community\n communityDegreeSum.set(currentComm, (communityDegreeSum.get(currentComm) ?? 0) - ki);\n const weightToCurrent = weightToComm.get(currentComm) ?? 0;\n communityInternalWeight.set(\n currentComm,\n (communityInternalWeight.get(currentComm) ?? 0) - weightToCurrent,\n );\n\n // Find best community\n let bestComm = currentComm;\n let bestDelta = 0;\n\n for (const [candidateComm, weightToCandidate] of weightToComm) {\n const sigmaTot = communityDegreeSum.get(candidateComm) ?? 0;\n // Modularity gain of moving node to candidateComm\n const delta = weightToCandidate - resolution * (ki * sigmaTot) / m2;\n if (delta > bestDelta) {\n bestDelta = delta;\n bestComm = candidateComm;\n }\n }\n\n // Also consider staying (delta = 0, but need to re-add)\n // Move node to best community\n community.set(node, bestComm);\n communityDegreeSum.set(bestComm, (communityDegreeSum.get(bestComm) ?? 0) + ki);\n const weightToBest = weightToComm.get(bestComm) ?? 0;\n communityInternalWeight.set(\n bestComm,\n (communityInternalWeight.get(bestComm) ?? 0) + weightToBest,\n );\n\n if (bestComm !== currentComm) {\n improved = true;\n }\n }\n\n return improved;\n}\n\n/**\n * Compute modularity for a given partition.\n */\nfunction computeModularity(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): number {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return 0;\n\n let q = 0;\n for (const [node, neighbors] of graph.adj) {\n const ci = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === ci) {\n q += weight - resolution * (ki * (graph.degree.get(neighbor) ?? 0)) / m2;\n }\n }\n }\n return q / m2;\n}\n\n/**\n * Run Louvain community detection on the vault link graph.\n */\nfunction runLouvain(\n graph: Graph,\n resolution: number,\n): Map<string, number> {\n // Initialize: each node in its own community\n const community = new Map<string, number>();\n let nextComm = 0;\n for (const node of graph.nodes) {\n community.set(node, nextComm++);\n }\n\n // Phase 1: iteratively move nodes until no improvement\n const MAX_ITERATIONS = 20;\n for (let iter = 0; iter < MAX_ITERATIONS; iter++) {\n const improved = louvainPhase1(graph, community, resolution);\n if (!improved) break;\n }\n\n // Compact community IDs\n const commRemap = new Map<number, number>();\n let remapIdx = 0;\n for (const [, c] of community) {\n if (!commRemap.has(c)) {\n commRemap.set(c, remapIdx++);\n }\n }\n for (const [node, c] of community) {\n community.set(node, commRemap.get(c)!);\n }\n\n return community;\n}\n\n// ---------------------------------------------------------------------------\n// Label generation\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"the\", \"and\", \"for\", \"are\", \"but\", \"not\", \"you\", \"all\", \"can\", \"her\",\n \"was\", \"one\", \"our\", \"out\", \"has\", \"had\", \"how\", \"its\", \"may\", \"new\",\n \"now\", \"old\", \"see\", \"way\", \"who\", \"did\", \"get\", \"let\", \"say\", \"she\",\n \"too\", \"use\", \"from\", \"with\", \"this\", \"that\", \"will\", \"been\", \"have\",\n \"each\", \"make\", \"like\", \"long\", \"look\", \"many\", \"them\", \"then\", \"what\",\n \"when\", \"some\", \"time\", \"very\", \"your\", \"about\", \"could\", \"into\", \"just\",\n \"more\", \"note\", \"notes\", \"than\", \"over\",\n]);\n\nfunction generateCommunityLabel(titles: Array<string | null>): string {\n const wordCounts = new Map<string, number>();\n for (const title of titles) {\n if (!title) continue;\n const words = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted\n .slice(0, 3)\n .map(([w]) => w)\n .join(\" / \") || \"unnamed\";\n}\n\nfunction getTopFolder(path: string): string {\n const slash = path.indexOf(\"/\");\n return slash === -1 ? path : path.slice(0, slash);\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Detect communities in the vault link graph using the Louvain algorithm.\n * Returns clusters of densely connected notes with labels and cohesion scores.\n */\nexport async function zettelCommunities(\n backend: StorageBackend,\n opts?: CommunityOptions,\n): Promise<CommunityResult> {\n const minSize = opts?.minSize ?? 3;\n const maxCommunities = opts?.maxCommunities ?? 20;\n const resolution = opts?.resolution ?? 1.0;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n const graph = buildUndirectedGraph(linkGraph);\n\n if (graph.nodes.length === 0) {\n return {\n communities: [],\n totalNodes: 0,\n totalEdges: 0,\n modularity: 0,\n };\n }\n\n // Run Louvain\n const communityMap = runLouvain(graph, resolution);\n\n // Compute modularity\n const modularity = computeModularity(graph, communityMap, resolution);\n\n // Group nodes by community\n const groups = new Map<number, string[]>();\n for (const [node, comm] of communityMap) {\n const arr = groups.get(comm);\n if (arr) arr.push(node);\n else groups.set(comm, [node]);\n }\n\n // Get all vault files for title lookup\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n // Build community results\n const communities: Community[] = [];\n let commId = 0;\n\n for (const [, members] of groups) {\n if (members.length < minSize) continue;\n\n const memberSet = new Set(members);\n const titles = members.map((p) => titleMap.get(p) ?? null);\n const label = generateCommunityLabel(titles);\n\n // Compute internal degree per node\n const nodes: CommunityNode[] = members.map((path) => {\n const neighbors = graph.adj.get(path) ?? new Map();\n let internalDeg = 0;\n for (const [neighbor, weight] of neighbors) {\n if (memberSet.has(neighbor)) internalDeg += weight;\n }\n return {\n path,\n title: titleMap.get(path) ?? null,\n internalDegree: internalDeg,\n };\n });\n\n // Sort nodes by internal degree descending\n nodes.sort((a, b) => b.internalDegree - a.internalDegree);\n\n // Compute cohesion: ratio of actual internal edges to possible\n let internalEdges = 0;\n for (const node of nodes) {\n internalEdges += node.internalDegree;\n }\n internalEdges /= 2; // each edge counted twice\n const possibleEdges = (members.length * (members.length - 1)) / 2;\n const cohesion = possibleEdges > 0 ? Math.round((internalEdges / possibleEdges) * 1000) / 1000 : 0;\n\n // Top folders\n const folderCounts = new Map<string, number>();\n for (const path of members) {\n const folder = getTopFolder(path);\n folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);\n }\n const topFolders = [...folderCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([f]) => f);\n\n communities.push({\n id: commId++,\n label,\n nodes,\n size: members.length,\n cohesion,\n topFolders,\n });\n }\n\n // Sort by size descending\n communities.sort((a, b) => b.size - a.size);\n\n return {\n communities: communities.slice(0, maxCommunities),\n totalNodes: graph.nodes.length,\n totalEdges: graph.totalWeight,\n modularity: Math.round(modularity * 10000) / 10000,\n };\n}\n"],"mappings":"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"}
@@ -80,3 +80,141 @@ input_tokens=… cache_read_input_tokens=… duration_api_ms=…` land as design
80
80
  short-range replay buffer, not a warmable baseline; new-worker cold starts
81
81
  cannot be removed by a heartbeat, and the fix would have to live in the
82
82
  provider (explicit `cache_control` support) rather than in the daemon.
83
+
84
+ ## Interactive sessions
85
+
86
+ `sessions.cacheKeepalive` is a *different* problem from the worker keepalive
87
+ above: an anthropic Claude Code session's ephemeral prompt cache has a 1-hour
88
+ TTL. A cache **read** (the daemon typing a trivial prompt into an idle
89
+ session) refreshes that TTL at roughly 0.1x the input-token price of a normal
90
+ turn. If the cache is instead allowed to expire, the next real prompt has to
91
+ rebuild the whole context from scratch — an input-token rewrite billed at
92
+ 2x. So each beat that prevents one expiry is worth roughly 20x its own cost
93
+ (0.1x spent vs. 2x avoided over the same context size) — the break-even is
94
+ around 20 avoided expiries per beat's worth of spend, which is why the
95
+ feature caps itself at a handful of beats per idle stretch rather than
96
+ beating forever: past `maxBeats`, further beats are pure cost with nothing
97
+ left to protect (either the user came back, or the idle stretch has already
98
+ run past anything the TTL math pays for).
99
+
100
+ Unlike the worker keepalive, this cannot run on a fixed timer: an interactive
101
+ session's cache is only ever at risk while genuinely idle, and beating a
102
+ session the user is actively reading burns tokens and clutters the
103
+ transcript for zero benefit. So the daemon tick (`src/daemon/session-keepalive.ts`,
104
+ started from `src/daemon/daemon/scheduler.ts` only when enabled) runs every
105
+ 60 seconds and, per live interactive session, beats it only when *every* one
106
+ of these holds:
107
+
108
+ - idle time (time since the transcript file was last written) is at least
109
+ `idleMinutes`
110
+ - the local clock is inside `activeHours`
111
+ - the last known context size is at least `minContextTokens` (a session too
112
+ small to have paid for a 1h cache in the first place is not worth beating)
113
+ - the session is not mid-turn (its last transcript line is a finished
114
+ assistant turn, not a streaming one or an unanswered prompt)
115
+ - fewer than `maxBeats` beats have been sent since the last real (non-beat)
116
+ user prompt
117
+
118
+ A worker session is not excluded "for free": a `claude -p` worker running in
119
+ a worktree writes its own transcript under `~/.claude/projects` (Claude Code
120
+ encodes its worktree cwd — under `<workers.logDir>/worktrees` — as the
121
+ project-dir name), so an unguarded tick could beat a worker pane. The tick
122
+ instead checks each session's transcript path against the workers log dir's
123
+ worktrees directory (`isWorkerSession` in `session-keepalive.ts`) and skips
124
+ with `skipped:worker` when it matches.
125
+
126
+ ### Config
127
+
128
+ Off by default. Enable by editing the PAI config file directly (there is no
129
+ `pai config set` for this yet — see the config-CLI work landing separately):
130
+
131
+ ```json
132
+ {
133
+ "sessions": {
134
+ "cacheKeepalive": {
135
+ "enabled": true,
136
+ "idleMinutes": 50,
137
+ "maxBeats": 6,
138
+ "activeHours": "08:00-22:00",
139
+ "minContextTokens": 20000,
140
+ "prompt": "keepalive"
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ - `idleMinutes` — how long a session must sit untouched before it is beaten
147
+ (default 50, just under the 1h TTL).
148
+ - `maxBeats` — hard cap on consecutive beats since the last real prompt, so a
149
+ session left open overnight does not accrue an unbounded bill; resets the
150
+ moment a genuine new prompt appears.
151
+ - `activeHours` — `"HH:MM-HH:MM"`, local time; a window may wrap midnight
152
+ (e.g. `"22:00-06:00"`). Keeps beats confined to hours where a fresh cache
153
+ is actually likely to be used again soon.
154
+ - `minContextTokens` — skip sessions too small to have a cache worth
155
+ protecting.
156
+ - `prompt` — the literal text sent as the beat. The `UserPromptSubmit` hook
157
+ (`src/hooks/ts/user-prompt/whisper-rules.ts`, `isCacheKeepaliveBeat`)
158
+ recognizes this exact word and replies with the smallest possible
159
+ instruction ("reply with a single period, no tools"), suppressing every
160
+ other injected rule/advisor block for that one prompt — the point of the
161
+ beat is a cheap round trip, and injecting the usual ~5KB of rules into it
162
+ would erase most of the saving.
163
+
164
+ ### State file
165
+
166
+ Per-session beat counters live at `~/.claude/pai/session-keepalive.json`
167
+ (`PAI_HOME`-relative, rebuildable — a damaged file just resets counters, it
168
+ is never a reason to skip beating):
169
+
170
+ ```json
171
+ {
172
+ "<session-id>": {
173
+ "beats": 2,
174
+ "lastRealPromptKey": "<uuid-of-last-real-prompt>",
175
+ "lastBeatAt": "2026-09-20T10:00:00.000Z"
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### Ledger
181
+
182
+ Every tick decision — sent or skipped, with the reason — writes one line to
183
+ the same ledger `WORKER-KEEPALIVE` already uses
184
+ (`<workers.logDir>/ledger.log`). `session=` is the Claude transcript session
185
+ id (resolved from the AIBroker pane id via `claude-session-map.json`, see
186
+ below); `pane=` is the raw AIBroker/iTerm pane id, kept alongside it so both
187
+ identities are visible:
188
+
189
+ ```
190
+ 2026-09-20 10:00:00 SESSION-KEEPALIVE session=<id> pane=<pane-id> idle_min=52.3 context=84213 beat=1/6 result=sent
191
+ 2026-09-20 10:01:00 SESSION-KEEPALIVE session=<id> pane=<pane-id> idle_min=0.2 context=84501 beat=1/6 result=skipped:idle:0.2min
192
+ 2026-09-20 10:02:00 SESSION-KEEPALIVE pane=<pane-id> result=skipped:unmapped
193
+ ```
194
+
195
+ `fetchLiveSessions()` identifies a live session by its AIBroker/iTerm pane id,
196
+ not the Claude session id its transcript is named after — the status line
197
+ bridges the two on every refresh (`claude-session-map.json` in the workers
198
+ log dir, see `resolveClaudeSessionIdFromMap` in `session-keepalive.ts`). A
199
+ pane with no (fresh enough) mapped Claude session — too new, or a non-iTerm
200
+ terminal the status line never wrote an entry for — is skipped with
201
+ `skipped:unmapped` and sent no beat, since it has no transcript to read.
202
+
203
+ ### Checking payoff
204
+
205
+ `pai daemon keepalive` prints whether the feature is enabled, its current
206
+ parameters, every session's beat counter, and the last 10 ledger lines.
207
+
208
+ `pai audit tokens session` reports, on any session transcript, `idle gaps >
209
+ 60min: N` (how many times the session actually sat idle long enough to be at
210
+ risk) alongside `keepalive beats: N` (how many of that session's prompts were
211
+ beats rather than real turns) — the two numbers together show whether the
212
+ feature is firing where it is needed and how much it is costing to do so.
213
+
214
+ ### Caveat
215
+
216
+ A beat is still a real API call and, on a Max-plan subscription, still
217
+ counts against the plan's rate-limit window even though its token cost is
218
+ small — `maxBeats` and `activeHours` exist as much to bound that call count
219
+ as to bound spend. An operator on a tight rate window should keep `maxBeats`
220
+ low and `activeHours` narrow rather than assuming "cheap" means "free."
@@ -60,10 +60,16 @@ Generated man pages for every `pai` command area. Read any page in the terminal
60
60
  | `pai backup` | Backup registry, config, and Postgres database to PAI_HOME/backups/ |
61
61
  | `pai clear-names` | Recovery: wipe corrupted iTerm2 session name state. |
62
62
  | `pai config` | PAI_HOME namespace dir: path, migrate |
63
+ | `pai config get <path>` | Print one config value (dotted path, e.g. search.recencyBoostDays), masked if it looks like a secret. An object/array subtree prints as YAML; pass --json for JSON. |
64
+ | `pai config list` | Print the main PAI config as YAML (secrets masked). Defaults to what the file explicitly sets. |
63
65
  | `pai config migrate` | Move config.json, workers.yaml, whisper-rules.md, advisor-mode.json, session-state/, |
64
66
  | `pai config path` | Print the PAI_HOME namespace dir and each resolved per-user file |
67
+ | `pai config set <path> <value>` | Set one config value (dotted path). Value parsing: true/false, null, numbers, |
68
+ | `pai config unset <path>` | Remove one config value (dotted path), reverting it to the built-in default |
69
+ | `pai config yaml` | Convert config.json → config.yaml (and voices.json → voices.yaml), with a short |
65
70
  | `pai daemon` | PAI daemon management: serve, status, restart, install, uninstall, logs |
66
71
  | `pai daemon install` | Install daemon as a launchd service and update ~/.claude.json to use the shim |
72
+ | `pai daemon keepalive` | Show interactive-session cache keepalive config, per-session beat counters, and recent ledger lines |
67
73
  | `pai daemon logs` | Tail the daemon log (/tmp/pai-daemon.log) |
68
74
  | `pai daemon migrate` | Migrate federation data from SQLite to PostgreSQL |
69
75
  | `pai daemon restart` | Send SIGTERM to the running daemon (launchd will restart it) |
@@ -214,6 +220,7 @@ Generated man pages for every `pai` command area. Read any page in the terminal
214
220
  | `pai topic check <context>` | Check whether context text has drifted to a different project. |
215
221
  | `pai update` | Update PAI from GitHub (git pull + rebuild + daemon restart). Preserves local customizations. |
216
222
  | `pai worker` | Run subagents on configured worker providers: run, ps, follow, replay, |
223
+ | `pai worker capability [name] [providers]` | Which provider(s) serve a capability (e.g. image), independent of any one |
217
224
  | `pai worker classes` | Classes: which provider serves draft / implement / review / … |
218
225
  | `pai worker classes list` | List classes and their targets (default action) |
219
226
  | `pai worker classes set <class> [target]` | Point a class at a provider (or provider/fast), or give only constraints: |
@@ -60,6 +60,7 @@ Cache/input/output token split for a session transcript (default: newest)
60
60
  |--------|-------------|---------|
61
61
  | `--ctx-threshold <n>` | Per-turn context size above which a turn counts as 'above threshold' (default 200000) | |
62
62
  | `--history [n]` | List first-turn context of the newest n sessions in this project's transcript directory (default 20) | |
63
+ | `--turn <n>` | Attribute API call n's context growth to the transcript lines injected since the previous call | |
63
64
 
64
65
 
65
66
  ### pai audit tokens spawn