@tekmidian/pai 0.5.7 → 0.6.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 (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +87 -1
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1482 -1628
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +11 -8
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-2ND5WO2j.mjs → daemon-D3hYb5_C.mjs} +669 -218
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/db-DdUperSl.mjs +110 -0
  18. package/dist/db-DdUperSl.mjs.map +1 -0
  19. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  20. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  21. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  22. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  23. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  24. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  25. package/dist/helpers-BEST-4Gx.mjs +420 -0
  26. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  27. package/dist/hooks/capture-all-events.mjs +2 -2
  28. package/dist/hooks/capture-all-events.mjs.map +3 -3
  29. package/dist/hooks/capture-session-summary.mjs +38 -0
  30. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  31. package/dist/hooks/cleanup-session-files.mjs +6 -12
  32. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  33. package/dist/hooks/context-compression-hook.mjs +93 -104
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/initialize-session.mjs +14 -11
  36. package/dist/hooks/initialize-session.mjs.map +4 -4
  37. package/dist/hooks/inject-observations.mjs +220 -0
  38. package/dist/hooks/inject-observations.mjs.map +7 -0
  39. package/dist/hooks/load-core-context.mjs +2 -2
  40. package/dist/hooks/load-core-context.mjs.map +3 -3
  41. package/dist/hooks/load-project-context.mjs +90 -91
  42. package/dist/hooks/load-project-context.mjs.map +4 -4
  43. package/dist/hooks/observe.mjs +354 -0
  44. package/dist/hooks/observe.mjs.map +7 -0
  45. package/dist/hooks/stop-hook.mjs +94 -107
  46. package/dist/hooks/stop-hook.mjs.map +4 -4
  47. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  48. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  49. package/dist/index.d.mts +30 -7
  50. package/dist/index.d.mts.map +1 -1
  51. package/dist/index.mjs +5 -8
  52. package/dist/indexer-D53l5d1U.mjs +1 -0
  53. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  54. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  55. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  56. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  57. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  58. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  59. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  60. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  61. package/dist/note-context-BK24bX8Y.mjs +126 -0
  62. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  63. package/dist/postgres-CKf-EDtS.mjs +846 -0
  64. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  65. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  66. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  67. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  68. package/dist/search-DC1qhkKn.mjs.map +1 -0
  69. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  70. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  71. package/dist/state-C6_vqz7w.mjs +102 -0
  72. package/dist/state-C6_vqz7w.mjs.map +1 -0
  73. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  74. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  75. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  76. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  77. package/dist/themes-BvYF0W8T.mjs +148 -0
  78. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  79. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  80. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  81. package/dist/trace-CRx9lPuc.mjs +137 -0
  82. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  83. package/dist/{vault-indexer-k-kUlaZ-.mjs → vault-indexer-Bi2cRmn7.mjs} +134 -132
  84. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  85. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  86. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  87. package/package.json +1 -2
  88. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  89. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  90. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  91. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  92. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  93. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  94. package/src/hooks/ts/lib/project-utils.ts +40 -1018
  95. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  96. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  97. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  98. package/dist/chunker-CbnBe0s0.mjs +0 -191
  99. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  100. package/dist/config-Cf92lGX_.mjs.map +0 -1
  101. package/dist/daemon-2ND5WO2j.mjs.map +0 -1
  102. package/dist/db-Dp8VXIMR.mjs +0 -212
  103. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  104. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  105. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  106. package/dist/mcp/index.d.mts +0 -1
  107. package/dist/mcp/index.mjs +0 -500
  108. package/dist/mcp/index.mjs.map +0 -1
  109. package/dist/postgres-FXrHDPcE.mjs +0 -358
  110. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  111. package/dist/schemas-BFIgGntb.mjs +0 -3405
  112. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  113. package/dist/search-_oHfguA5.mjs.map +0 -1
  114. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  115. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  116. package/dist/vault-indexer-k-kUlaZ-.mjs.map +0 -1
  117. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  118. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  119. package/templates/README.md +0 -181
  120. package/templates/skills/CORE/Aesthetic.md +0 -333
  121. package/templates/skills/CORE/CONSTITUTION.md +0 -1502
  122. package/templates/skills/CORE/HistorySystem.md +0 -427
  123. package/templates/skills/CORE/HookSystem.md +0 -1082
  124. package/templates/skills/CORE/Prompting.md +0 -509
  125. package/templates/skills/CORE/ProsodyAgentTemplate.md +0 -53
  126. package/templates/skills/CORE/ProsodyGuide.md +0 -416
  127. package/templates/skills/CORE/SKILL.md +0 -741
  128. package/templates/skills/CORE/SkillSystem.md +0 -213
  129. package/templates/skills/CORE/TerminalTabs.md +0 -119
  130. package/templates/skills/CORE/VOICE.md +0 -106
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -1,901 +0,0 @@
1
- import { a as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DGRAPAYb.mjs";
2
- import { i as searchMemoryHybrid } from "./search-_oHfguA5.mjs";
3
- import { basename, dirname } from "node:path";
4
-
5
- //#region src/zettelkasten/explore.ts
6
- function classifyEdge(source, target) {
7
- return dirname(source) === dirname(target) ? "sequential" : "associative";
8
- }
9
- function resolveStart(db, startNote) {
10
- const inFiles = db.prepare("SELECT vault_path FROM vault_files WHERE vault_path = ?").get(startNote);
11
- if (inFiles) return inFiles.vault_path;
12
- const alias = db.prepare("SELECT canonical_path FROM vault_aliases WHERE vault_path = ?").get(startNote);
13
- if (!alias) return null;
14
- const canonical = db.prepare("SELECT vault_path FROM vault_files WHERE vault_path = ?").get(alias.canonical_path);
15
- return canonical ? canonical.vault_path : null;
16
- }
17
- function getForwardNeighbors(db, path) {
18
- return db.prepare("SELECT target_path FROM vault_links WHERE source_path = ? AND target_path IS NOT NULL").all(path).map((r) => r.target_path);
19
- }
20
- function getBackwardNeighbors(db, path) {
21
- return db.prepare("SELECT source_path FROM vault_links WHERE target_path = ?").all(path).map((r) => r.source_path);
22
- }
23
- function getFileInfo(db, path) {
24
- const file = db.prepare("SELECT title FROM vault_files WHERE vault_path = ?").get(path);
25
- const health = db.prepare("SELECT inbound_count, outbound_count FROM vault_health WHERE vault_path = ?").get(path);
26
- return {
27
- title: file?.title ?? null,
28
- inbound: health?.inbound_count ?? 0,
29
- outbound: health?.outbound_count ?? 0
30
- };
31
- }
32
- /**
33
- * Traverse the Zettelkasten link graph using BFS, following chains of thought
34
- * from a starting note up to a configurable depth.
35
- */
36
- function zettelExplore(db, opts) {
37
- const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);
38
- const direction = opts.direction ?? "both";
39
- const mode = opts.mode ?? "all";
40
- const root = resolveStart(db, opts.startNote);
41
- if (!root) return {
42
- root: opts.startNote,
43
- nodes: [],
44
- edges: [],
45
- branchingPoints: [],
46
- maxDepthReached: false
47
- };
48
- const visited = new Set([root]);
49
- const nodes = [];
50
- const edges = [];
51
- let maxDepthReached = false;
52
- const queue = [{
53
- path: root,
54
- depth: 0
55
- }];
56
- while (queue.length > 0) {
57
- const current = queue.shift();
58
- if (current.depth >= depth) {
59
- maxDepthReached = true;
60
- continue;
61
- }
62
- const neighbors = [];
63
- if (direction === "forward" || direction === "both") for (const n of getForwardNeighbors(db, current.path)) neighbors.push({
64
- neighbor: n,
65
- from: current.path,
66
- to: n
67
- });
68
- if (direction === "backward" || direction === "both") for (const n of getBackwardNeighbors(db, current.path)) neighbors.push({
69
- neighbor: n,
70
- from: n,
71
- to: current.path
72
- });
73
- for (const { neighbor, from, to } of neighbors) {
74
- const edgeType = classifyEdge(from, to);
75
- if (mode !== "all" && edgeType !== mode) continue;
76
- `${from}${to}`;
77
- if (!edges.some((e) => e.from === from && e.to === to)) edges.push({
78
- from,
79
- to,
80
- type: edgeType
81
- });
82
- if (!visited.has(neighbor)) {
83
- visited.add(neighbor);
84
- const info = getFileInfo(db, neighbor);
85
- nodes.push({
86
- path: neighbor,
87
- title: info.title,
88
- depth: current.depth + 1,
89
- linkType: edgeType,
90
- inbound: info.inbound,
91
- outbound: info.outbound
92
- });
93
- queue.push({
94
- path: neighbor,
95
- depth: current.depth + 1
96
- });
97
- }
98
- }
99
- }
100
- const branchingPoints = nodes.filter((n) => n.outbound > 2).map((n) => n.path);
101
- if (getFileInfo(db, root).outbound > 2) branchingPoints.unshift(root);
102
- return {
103
- root,
104
- nodes,
105
- edges,
106
- branchingPoints,
107
- maxDepthReached
108
- };
109
- }
110
-
111
- //#endregion
112
- //#region src/zettelkasten/surprise.ts
113
- const MAX_CHUNKS$2 = 5e3;
114
- const BFS_HOP_CAP = 20;
115
- function getFileEmbeddings(db, projectId) {
116
- const rows = db.prepare(`SELECT path, embedding, text FROM memory_chunks
117
- WHERE project_id = ? AND embedding IS NOT NULL
118
- ORDER BY path, start_line
119
- LIMIT ?`).all(projectId, MAX_CHUNKS$2);
120
- const byPath = /* @__PURE__ */ new Map();
121
- for (const row of rows) {
122
- const vec = deserializeEmbedding(row.embedding);
123
- const entry = byPath.get(row.path);
124
- if (!entry) byPath.set(row.path, {
125
- sum: new Float32Array(vec),
126
- count: 1,
127
- text: row.text
128
- });
129
- else {
130
- for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
131
- entry.count++;
132
- }
133
- }
134
- const result = /* @__PURE__ */ new Map();
135
- for (const [path, { sum, count, text }] of byPath) {
136
- const avg = new Float32Array(sum.length);
137
- for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
138
- result.set(path, {
139
- embedding: avg,
140
- text
141
- });
142
- }
143
- return result;
144
- }
145
- function getReferenceEmbedding(db, projectId, path) {
146
- const rows = db.prepare(`SELECT embedding FROM memory_chunks
147
- WHERE project_id = ? AND path = ? AND embedding IS NOT NULL`).all(projectId, path);
148
- if (rows.length === 0) return {
149
- embedding: new Float32Array(0),
150
- found: false
151
- };
152
- const dim = deserializeEmbedding(rows[0].embedding).length;
153
- const sum = new Float32Array(dim);
154
- for (const row of rows) {
155
- const vec = deserializeEmbedding(row.embedding);
156
- for (let i = 0; i < dim; i++) sum[i] += vec[i];
157
- }
158
- const avg = new Float32Array(dim);
159
- for (let i = 0; i < dim; i++) avg[i] = sum[i] / rows.length;
160
- return {
161
- embedding: avg,
162
- found: true
163
- };
164
- }
165
- function bfsGraphDistance(db, source, target) {
166
- if (source === target) return 0;
167
- const visited = new Set([source]);
168
- const queue = [{
169
- path: source,
170
- hops: 0
171
- }];
172
- while (queue.length > 0) {
173
- const { path, hops } = queue.shift();
174
- if (hops >= BFS_HOP_CAP) continue;
175
- const neighbors = db.prepare(`SELECT target_path AS neighbor FROM vault_links
176
- WHERE source_path = ? AND target_path IS NOT NULL
177
- UNION
178
- SELECT source_path AS neighbor FROM vault_links
179
- WHERE target_path = ?`).all(path, path);
180
- for (const { neighbor } of neighbors) {
181
- if (neighbor === target) return hops + 1;
182
- if (!visited.has(neighbor)) {
183
- visited.add(neighbor);
184
- queue.push({
185
- path: neighbor,
186
- hops: hops + 1
187
- });
188
- }
189
- }
190
- }
191
- return Infinity;
192
- }
193
- function getBestChunkText(db, projectId, path, refEmbedding) {
194
- const rows = db.prepare(`SELECT text, embedding FROM memory_chunks
195
- WHERE project_id = ? AND path = ? AND embedding IS NOT NULL
196
- LIMIT 20`).all(projectId, path);
197
- if (rows.length === 0) return "";
198
- let bestText = rows[0].text;
199
- let bestSim = -Infinity;
200
- for (const row of rows) {
201
- const sim = cosineSimilarity(refEmbedding, deserializeEmbedding(row.embedding));
202
- if (sim > bestSim) {
203
- bestSim = sim;
204
- bestText = row.text;
205
- }
206
- }
207
- return bestText.trim().slice(0, 200);
208
- }
209
- /**
210
- * Find notes that are semantically similar to a reference note but graph-distant —
211
- * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.
212
- */
213
- async function zettelSurprise(db, opts) {
214
- const limit = opts.limit ?? 10;
215
- const minSimilarity = opts.minSimilarity ?? .3;
216
- const minGraphDistance = opts.minGraphDistance ?? 3;
217
- let { embedding: refEmbedding, found } = getReferenceEmbedding(db, opts.vaultProjectId, opts.referencePath);
218
- if (!found) refEmbedding = await generateEmbedding(db.prepare("SELECT title FROM vault_files WHERE vault_path = ?").get(opts.referencePath)?.title ?? opts.referencePath, true);
219
- const allFileEmbeddings = getFileEmbeddings(db, opts.vaultProjectId);
220
- allFileEmbeddings.delete(opts.referencePath);
221
- const semanticCandidates = [];
222
- for (const [path, { embedding }] of allFileEmbeddings) {
223
- const sim = cosineSimilarity(refEmbedding, embedding);
224
- if (sim >= minSimilarity) semanticCandidates.push({
225
- path,
226
- sim
227
- });
228
- }
229
- const results = [];
230
- for (const { path, sim } of semanticCandidates) {
231
- const graphDistance = bfsGraphDistance(db, opts.referencePath, path);
232
- const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;
233
- if (effectiveDistance < minGraphDistance) continue;
234
- const file = db.prepare("SELECT title FROM vault_files WHERE vault_path = ?").get(path);
235
- const surpriseScore = sim * Math.log2(effectiveDistance + 1);
236
- const sharedSnippet = getBestChunkText(db, opts.vaultProjectId, path, refEmbedding);
237
- results.push({
238
- path,
239
- title: file?.title ?? null,
240
- cosineSimilarity: sim,
241
- graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,
242
- surpriseScore,
243
- sharedSnippet
244
- });
245
- }
246
- results.sort((a, b) => b.surpriseScore - a.surpriseScore);
247
- return results.slice(0, limit);
248
- }
249
-
250
- //#endregion
251
- //#region src/zettelkasten/converse.ts
252
- /** Extract the top-level folder from a vault path (first path segment). */
253
- function extractDomain(vaultPath) {
254
- const slash = vaultPath.indexOf("/");
255
- return slash === -1 ? vaultPath : vaultPath.slice(0, slash);
256
- }
257
- /**
258
- * Expand one level of graph neighbors for a set of paths.
259
- * Returns all outbound and inbound neighbor paths (excluding already-visited).
260
- */
261
- function expandNeighbors(db, paths) {
262
- if (paths.size === 0) return [];
263
- const placeholders = Array.from(paths).map(() => "?").join(", ");
264
- const pathList = Array.from(paths);
265
- const forward = db.prepare(`SELECT DISTINCT target_path FROM vault_links WHERE source_path IN (${placeholders}) AND target_path IS NOT NULL`).all(...pathList);
266
- const backward = db.prepare(`SELECT DISTINCT source_path FROM vault_links WHERE target_path IN (${placeholders})`).all(...pathList);
267
- const neighbors = [];
268
- for (const r of forward) neighbors.push(r.target_path);
269
- for (const r of backward) neighbors.push(r.source_path);
270
- return neighbors;
271
- }
272
- /**
273
- * Look up the title for a single vault path.
274
- * Returns null when the path is not found in vault_files.
275
- */
276
- function getTitle(db, path) {
277
- return db.prepare("SELECT title FROM vault_files WHERE vault_path = ?").get(path)?.title ?? null;
278
- }
279
- /**
280
- * Count inbound links for a path from vault_health.
281
- * Used as a tiebreaker when trimming neighbor-only notes.
282
- */
283
- function getInboundCount(db, path) {
284
- return db.prepare("SELECT inbound_count FROM vault_health WHERE vault_path = ?").get(path)?.inbound_count ?? 0;
285
- }
286
- /**
287
- * Let the vault "talk back" — find notes relevant to a question, expand
288
- * through the link graph, identify cross-domain connections, and return a
289
- * structured result including a synthesis prompt for an AI to generate insights.
290
- */
291
- async function zettelConverse(db, opts) {
292
- const depth = Math.max(opts.depth ?? 2, 0);
293
- const limit = Math.max(opts.limit ?? 15, 1);
294
- const candidateLimit = 20;
295
- const queryEmbedding = await generateEmbedding(opts.question, true);
296
- const searchResults = searchMemoryHybrid(db, opts.question, queryEmbedding, {
297
- projectIds: [opts.vaultProjectId],
298
- maxResults: candidateLimit
299
- });
300
- const searchHits = /* @__PURE__ */ new Map();
301
- for (const r of searchResults) {
302
- const existing = searchHits.get(r.path);
303
- if (!existing || r.score > existing.score) searchHits.set(r.path, {
304
- score: r.score,
305
- snippet: r.snippet
306
- });
307
- }
308
- const allPaths = new Set(searchHits.keys());
309
- let frontier = new Set(searchHits.keys());
310
- for (let d = 0; d < depth; d++) {
311
- const neighbors = expandNeighbors(db, frontier);
312
- const newFrontier = /* @__PURE__ */ new Set();
313
- for (const n of neighbors) if (!allPaths.has(n)) {
314
- allPaths.add(n);
315
- newFrontier.add(n);
316
- }
317
- if (newFrontier.size === 0) break;
318
- frontier = newFrontier;
319
- }
320
- const searchRanked = Array.from(searchHits.entries()).sort((a, b) => b[1].score - a[1].score).map(([path, info]) => ({
321
- path,
322
- ...info,
323
- isSearchResult: true
324
- }));
325
- const neighborRanked = Array.from(allPaths).filter((p) => !searchHits.has(p)).map((path) => ({
326
- path,
327
- score: 0,
328
- snippet: "",
329
- inbound: getInboundCount(db, path),
330
- isSearchResult: false
331
- })).sort((a, b) => b.inbound - a.inbound);
332
- const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);
333
- const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);
334
- const selectedSearchPaths = searchRanked.slice(0, limit);
335
- const selectedPaths = new Set([...selectedSearchPaths.map((r) => r.path), ...selectedNeighbors.map((r) => r.path)]);
336
- const relevantNotes = [];
337
- for (const r of selectedSearchPaths) {
338
- if (!selectedPaths.has(r.path)) continue;
339
- relevantNotes.push({
340
- path: r.path,
341
- title: getTitle(db, r.path),
342
- snippet: r.snippet,
343
- score: r.score,
344
- domain: extractDomain(r.path)
345
- });
346
- }
347
- for (const r of selectedNeighbors) relevantNotes.push({
348
- path: r.path,
349
- title: getTitle(db, r.path),
350
- snippet: r.snippet,
351
- score: 0,
352
- domain: extractDomain(r.path)
353
- });
354
- let connections = [];
355
- if (selectedPaths.size > 0) {
356
- const pathList = Array.from(selectedPaths);
357
- const placeholders = pathList.map(() => "?").join(", ");
358
- const edgeRows = db.prepare(`SELECT source_path, target_path, COUNT(*) AS cnt
359
- FROM vault_links
360
- WHERE source_path IN (${placeholders})
361
- AND target_path IN (${placeholders})
362
- GROUP BY source_path, target_path`).all(...pathList, ...pathList);
363
- for (const row of edgeRows) connections.push({
364
- fromPath: row.source_path,
365
- toPath: row.target_path,
366
- fromDomain: extractDomain(row.source_path),
367
- toDomain: extractDomain(row.target_path),
368
- strength: row.cnt
369
- });
370
- }
371
- const domainSet = new Set(relevantNotes.map((n) => n.domain));
372
- const domains = Array.from(domainSet).sort();
373
- const crossDomainConnections = connections.filter((c) => c.fromDomain !== c.toDomain);
374
- const notesSummary = relevantNotes.map((n, i) => {
375
- const title = n.title ? `"${n.title}"` : "(untitled)";
376
- const domain = n.domain;
377
- const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : " [context]";
378
- const snippet = n.snippet.trim().slice(0, 300);
379
- return `${i + 1}. [${domain}] ${title}${scoreLabel}\n Path: ${n.path}\n "${snippet}"`;
380
- }).join("\n\n");
381
- const connectionSummary = crossDomainConnections.length > 0 ? crossDomainConnections.map((c) => `- "${c.fromPath}" (${c.fromDomain}) → "${c.toPath}" (${c.toDomain}) [strength: ${c.strength}]`).join("\n") : "(no cross-domain connections found)";
382
- const domainList = domains.join(", ");
383
- return {
384
- relevantNotes,
385
- connections: crossDomainConnections,
386
- domains,
387
- synthesisPrompt: `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:
388
-
389
- QUESTION: ${opts.question}
390
-
391
- ---
392
-
393
- RELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):
394
-
395
- ${notesSummary}
396
-
397
- ---
398
-
399
- CROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):
400
-
401
- ${connectionSummary}
402
-
403
- ---
404
-
405
- SYNTHESIS TASK:
406
-
407
- Based on these notes and the connections between them, please:
408
-
409
- 1. Identify the key insights that emerge in direct response to the question.
410
- 2. Highlight any unexpected connections between notes from different domains (${domainList}).
411
- 3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.
412
- 4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.
413
- 5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.
414
-
415
- Think like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`
416
- };
417
- }
418
-
419
- //#endregion
420
- //#region src/zettelkasten/themes.ts
421
- const MAX_CHUNKS$1 = 5e3;
422
- const STOP_WORDS = new Set([
423
- "a",
424
- "an",
425
- "the",
426
- "and",
427
- "or",
428
- "but",
429
- "in",
430
- "on",
431
- "at",
432
- "to",
433
- "for",
434
- "of",
435
- "with",
436
- "by",
437
- "from",
438
- "is",
439
- "it",
440
- "as",
441
- "be",
442
- "was",
443
- "are",
444
- "has",
445
- "had",
446
- "have",
447
- "not",
448
- "this",
449
- "that",
450
- "i",
451
- "my",
452
- "we",
453
- "our",
454
- "new",
455
- "note",
456
- "untitled",
457
- "page",
458
- "file",
459
- "doc"
460
- ]);
461
- function getTopFolder(vaultPath) {
462
- const parts = vaultPath.split("/");
463
- return parts.length > 1 ? parts[0] : "";
464
- }
465
- function generateLabel(titles) {
466
- const wordCounts = /* @__PURE__ */ new Map();
467
- for (const title of titles) {
468
- if (!title) continue;
469
- const words = title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
470
- for (const word of words) wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
471
- }
472
- return [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w).join(" / ");
473
- }
474
- function computeLinkedRatio(db, paths) {
475
- if (paths.length < 2) return 0;
476
- const totalPairs = paths.length * (paths.length - 1) / 2;
477
- const pathSet = new Set(paths);
478
- let linkedPairs = 0;
479
- for (const path of paths) {
480
- const rows = db.prepare(`SELECT target_path FROM vault_links
481
- WHERE source_path = ? AND target_path IS NOT NULL`).all(path);
482
- for (const { target_path } of rows) if (pathSet.has(target_path)) linkedPairs++;
483
- }
484
- const uniquePairs = linkedPairs / 2;
485
- return Math.min(1, uniquePairs / totalPairs);
486
- }
487
- function averageEmbeddings(embeddings) {
488
- if (embeddings.length === 0) return new Float32Array(0);
489
- const dim = embeddings[0].length;
490
- const sum = new Float32Array(dim);
491
- for (const vec of embeddings) for (let i = 0; i < dim; i++) sum[i] += vec[i];
492
- const avg = new Float32Array(dim);
493
- for (let i = 0; i < dim; i++) avg[i] = sum[i] / embeddings.length;
494
- return avg;
495
- }
496
- /**
497
- * Detect emerging themes in recently-modified notes using agglomerative single-linkage
498
- * clustering of note-level embeddings.
499
- */
500
- async function zettelThemes(db, opts) {
501
- const lookbackDays = opts.lookbackDays ?? 30;
502
- const minClusterSize = opts.minClusterSize ?? 3;
503
- const maxThemes = opts.maxThemes ?? 10;
504
- const similarityThreshold = opts.similarityThreshold ?? .65;
505
- const now = Date.now();
506
- const from = now - lookbackDays * 864e5;
507
- const recentNotes = db.prepare(`SELECT vault_path, title, indexed_at FROM vault_files WHERE indexed_at > ?`).all(from);
508
- const chunkRows = db.prepare(`SELECT path, embedding FROM memory_chunks
509
- WHERE project_id = ? AND embedding IS NOT NULL
510
- ORDER BY path, start_line
511
- LIMIT ?`).all(opts.vaultProjectId, MAX_CHUNKS$1);
512
- const embeddingsByPath = /* @__PURE__ */ new Map();
513
- for (const row of chunkRows) {
514
- const vec = deserializeEmbedding(row.embedding);
515
- const arr = embeddingsByPath.get(row.path);
516
- if (!arr) embeddingsByPath.set(row.path, [vec]);
517
- else arr.push(vec);
518
- }
519
- const fileEmbeddings = /* @__PURE__ */ new Map();
520
- for (const [path, vecs] of embeddingsByPath) fileEmbeddings.set(path, averageEmbeddings(vecs));
521
- const clusters = [];
522
- for (const note of recentNotes) {
523
- const embedding = fileEmbeddings.get(note.vault_path);
524
- if (!embedding) continue;
525
- clusters.push({
526
- paths: [note.vault_path],
527
- titles: [note.title],
528
- indexedAts: [note.indexed_at],
529
- centroid: embedding
530
- });
531
- }
532
- const totalNotesAnalyzed = clusters.length;
533
- let merged = true;
534
- while (merged && clusters.length > 1) {
535
- merged = false;
536
- let bestSim = similarityThreshold;
537
- let bestI = -1;
538
- let bestJ = -1;
539
- for (let i = 0; i < clusters.length; i++) for (let j = i + 1; j < clusters.length; j++) {
540
- const sim = cosineSimilarity(clusters[i].centroid, clusters[j].centroid);
541
- if (sim > bestSim) {
542
- bestSim = sim;
543
- bestI = i;
544
- bestJ = j;
545
- }
546
- }
547
- if (bestI === -1) break;
548
- const ci = clusters[bestI];
549
- const cj = clusters[bestJ];
550
- const mergedPaths = [...ci.paths, ...cj.paths];
551
- const mergedTitles = [...ci.titles, ...cj.titles];
552
- const mergedIndexedAts = [...ci.indexedAts, ...cj.indexedAts];
553
- const memberEmbeddings = [];
554
- for (const p of mergedPaths) {
555
- const emb = fileEmbeddings.get(p);
556
- if (emb) memberEmbeddings.push(emb);
557
- }
558
- clusters[bestI] = {
559
- paths: mergedPaths,
560
- titles: mergedTitles,
561
- indexedAts: mergedIndexedAts,
562
- centroid: averageEmbeddings(memberEmbeddings)
563
- };
564
- clusters.splice(bestJ, 1);
565
- merged = true;
566
- }
567
- const themes = [];
568
- let clusterIndex = 0;
569
- for (const cluster of clusters) {
570
- if (cluster.paths.length < minClusterSize) continue;
571
- const label = generateLabel(cluster.titles) || `Theme ${clusterIndex + 1}`;
572
- const avgRecency = cluster.indexedAts.reduce((sum, t) => sum + t, 0) / cluster.indexedAts.length;
573
- const folderDiversity = new Set(cluster.paths.map(getTopFolder)).size / cluster.paths.length;
574
- const linkedRatio = computeLinkedRatio(db, cluster.paths);
575
- const suggestIndexNote = linkedRatio < .3 && cluster.paths.length >= 5;
576
- themes.push({
577
- id: clusterIndex++,
578
- label,
579
- notes: cluster.paths.map((path, idx) => ({
580
- path,
581
- title: cluster.titles[idx]
582
- })),
583
- size: cluster.paths.length,
584
- folderDiversity,
585
- avgRecency,
586
- linkedRatio,
587
- suggestIndexNote
588
- });
589
- }
590
- themes.sort((a, b) => b.size * b.folderDiversity * (b.avgRecency / now) - a.size * a.folderDiversity * (a.avgRecency / now));
591
- return {
592
- themes: themes.slice(0, maxThemes),
593
- totalNotesAnalyzed,
594
- timeWindow: {
595
- from,
596
- to: now
597
- }
598
- };
599
- }
600
-
601
- //#endregion
602
- //#region src/zettelkasten/health.ts
603
- function countComponents(nodes, edges) {
604
- if (nodes.length === 0) return 0;
605
- const parent = /* @__PURE__ */ new Map();
606
- const rank = /* @__PURE__ */ new Map();
607
- for (const n of nodes) {
608
- parent.set(n, n);
609
- rank.set(n, 0);
610
- }
611
- function find(x) {
612
- let root = x;
613
- while (parent.get(root) !== root) root = parent.get(root);
614
- let current = x;
615
- while (current !== root) {
616
- const next = parent.get(current);
617
- parent.set(current, root);
618
- current = next;
619
- }
620
- return root;
621
- }
622
- function union(a, b) {
623
- const ra = find(a);
624
- const rb = find(b);
625
- if (ra === rb) return;
626
- const rankA = rank.get(ra) ?? 0;
627
- const rankB = rank.get(rb) ?? 0;
628
- if (rankA < rankB) parent.set(ra, rb);
629
- else if (rankA > rankB) parent.set(rb, ra);
630
- else {
631
- parent.set(rb, ra);
632
- rank.set(ra, rankA + 1);
633
- }
634
- }
635
- for (const { source, target } of edges) if (parent.has(source) && parent.has(target)) union(source, target);
636
- const roots = /* @__PURE__ */ new Set();
637
- for (const n of nodes) roots.add(find(n));
638
- return roots.size;
639
- }
640
- /**
641
- * Audit the structural health of the Zettelkasten vault using graph metrics.
642
- * Designed to complete in under 60ms for a full vault.
643
- */
644
- function zettelHealth(db, opts) {
645
- const options = opts ?? {};
646
- const scope = options.scope ?? "full";
647
- const include = options.include ?? [
648
- "dead_links",
649
- "orphans",
650
- "disconnected",
651
- "low_connectivity"
652
- ];
653
- const computedAt = Date.now();
654
- let totalFiles = 0;
655
- if (scope === "full") totalFiles = db.prepare("SELECT COUNT(*) AS n FROM vault_files").get().n;
656
- else if (scope === "project") {
657
- const prefix = options.projectPath ?? "";
658
- totalFiles = db.prepare("SELECT COUNT(*) AS n FROM vault_files WHERE vault_path LIKE ? || '%'").get(prefix).n;
659
- } else {
660
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
661
- totalFiles = db.prepare("SELECT COUNT(*) AS n FROM vault_files WHERE indexed_at > ?").get(cutoff).n;
662
- }
663
- let totalLinks = 0;
664
- if (scope === "full") totalLinks = db.prepare("SELECT COUNT(*) AS n FROM vault_links").get().n;
665
- else if (scope === "project") {
666
- const prefix = options.projectPath ?? "";
667
- totalLinks = db.prepare("SELECT COUNT(*) AS n FROM vault_links WHERE source_path LIKE ? || '%'").get(prefix).n;
668
- } else {
669
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
670
- totalLinks = db.prepare("SELECT COUNT(*) AS n FROM vault_links WHERE source_path IN (SELECT vault_path FROM vault_files WHERE indexed_at > ?)").get(cutoff).n;
671
- }
672
- let deadLinks = [];
673
- if (include.includes("dead_links")) if (scope === "full") deadLinks = db.prepare("SELECT source_path, target_raw, line_number FROM vault_links WHERE target_path IS NULL").all().map((r) => ({
674
- sourcePath: r.source_path,
675
- targetRaw: r.target_raw,
676
- lineNumber: r.line_number
677
- }));
678
- else if (scope === "project") {
679
- const prefix = options.projectPath ?? "";
680
- deadLinks = db.prepare("SELECT source_path, target_raw, line_number FROM vault_links WHERE target_path IS NULL AND source_path LIKE ? || '%'").all(prefix).map((r) => ({
681
- sourcePath: r.source_path,
682
- targetRaw: r.target_raw,
683
- lineNumber: r.line_number
684
- }));
685
- } else {
686
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
687
- deadLinks = db.prepare("SELECT source_path, target_raw, line_number FROM vault_links WHERE target_path IS NULL AND source_path IN (SELECT vault_path FROM vault_files WHERE indexed_at > ?)").all(cutoff).map((r) => ({
688
- sourcePath: r.source_path,
689
- targetRaw: r.target_raw,
690
- lineNumber: r.line_number
691
- }));
692
- }
693
- let orphans = [];
694
- if (include.includes("orphans")) if (scope === "full") orphans = db.prepare("SELECT vault_path FROM vault_health WHERE is_orphan = 1").all().map((r) => r.vault_path);
695
- else if (scope === "project") {
696
- const prefix = options.projectPath ?? "";
697
- orphans = db.prepare("SELECT vault_path FROM vault_health WHERE is_orphan = 1 AND vault_path LIKE ? || '%'").all(prefix).map((r) => r.vault_path);
698
- } else {
699
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
700
- orphans = db.prepare("SELECT vh.vault_path FROM vault_health vh JOIN vault_files vf ON vh.vault_path = vf.vault_path WHERE vh.is_orphan = 1 AND vf.indexed_at > ?").all(cutoff).map((r) => r.vault_path);
701
- }
702
- let disconnectedClusters = 1;
703
- if (include.includes("disconnected")) {
704
- let allNodes;
705
- let allEdges;
706
- if (scope === "full") {
707
- allNodes = db.prepare("SELECT vault_path FROM vault_files").all().map((r) => r.vault_path);
708
- allEdges = db.prepare("SELECT DISTINCT source_path AS source, target_path AS target FROM vault_links WHERE target_path IS NOT NULL").all();
709
- } else if (scope === "project") {
710
- const prefix = options.projectPath ?? "";
711
- allNodes = db.prepare("SELECT vault_path FROM vault_files WHERE vault_path LIKE ? || '%'").all(prefix).map((r) => r.vault_path);
712
- allEdges = db.prepare("SELECT DISTINCT source_path AS source, target_path AS target FROM vault_links WHERE target_path IS NOT NULL AND source_path LIKE ? || '%'").all(prefix);
713
- } else {
714
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
715
- allNodes = db.prepare("SELECT vault_path FROM vault_files WHERE indexed_at > ?").all(cutoff).map((r) => r.vault_path);
716
- allEdges = db.prepare("SELECT DISTINCT source_path AS source, target_path AS target FROM vault_links WHERE target_path IS NOT NULL AND source_path IN (SELECT vault_path FROM vault_files WHERE indexed_at > ?)").all(cutoff);
717
- }
718
- disconnectedClusters = countComponents(allNodes, allEdges);
719
- }
720
- let lowConnectivity = [];
721
- if (include.includes("low_connectivity")) if (scope === "full") lowConnectivity = db.prepare("SELECT vault_path FROM vault_health WHERE inbound_count + outbound_count <= 1").all().map((r) => r.vault_path);
722
- else if (scope === "project") {
723
- const prefix = options.projectPath ?? "";
724
- lowConnectivity = db.prepare("SELECT vault_path FROM vault_health WHERE inbound_count + outbound_count <= 1 AND vault_path LIKE ? || '%'").all(prefix).map((r) => r.vault_path);
725
- } else {
726
- const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
727
- lowConnectivity = db.prepare("SELECT vh.vault_path FROM vault_health vh JOIN vault_files vf ON vh.vault_path = vf.vault_path WHERE vh.inbound_count + vh.outbound_count <= 1 AND vf.indexed_at > ?").all(cutoff).map((r) => r.vault_path);
728
- }
729
- const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;
730
- const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;
731
- const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;
732
- const healthScore = Math.round(100 * (1 - deadRatio) * (1 - orphanRatio * .5) * (1 - lowConnRatio * .3));
733
- return {
734
- totalFiles,
735
- totalLinks,
736
- deadLinks,
737
- orphans,
738
- disconnectedClusters,
739
- lowConnectivity,
740
- healthScore,
741
- computedAt
742
- };
743
- }
744
-
745
- //#endregion
746
- //#region src/zettelkasten/suggest.ts
747
- const MAX_CHUNKS = 5e3;
748
- const SEMANTIC_WEIGHT = .5;
749
- const TAG_WEIGHT = .2;
750
- const NEIGHBOR_WEIGHT = .3;
751
- function extractTagsFromChunkTexts(texts) {
752
- const tags = /* @__PURE__ */ new Set();
753
- for (const text of texts) {
754
- const match = text.match(/^tags:\s*\n((?:[ \t]*-[ \t]*.+\n?)*)/m);
755
- if (!match) continue;
756
- const lines = match[1].split("\n");
757
- for (const line of lines) {
758
- const tagMatch = line.match(/^[ \t]*-[ \t]*(.+)/);
759
- if (tagMatch) {
760
- const tag = tagMatch[1].trim().toLowerCase();
761
- if (tag) tags.add(tag);
762
- }
763
- }
764
- }
765
- return tags;
766
- }
767
- function getFileAvgEmbedding(db, projectId, path) {
768
- const rows = db.prepare(`SELECT embedding FROM memory_chunks
769
- WHERE project_id = ? AND path = ? AND embedding IS NOT NULL`).all(projectId, path);
770
- if (rows.length === 0) return null;
771
- const first = deserializeEmbedding(rows[0].embedding);
772
- const sum = new Float32Array(first.length);
773
- for (const row of rows) {
774
- const vec = deserializeEmbedding(row.embedding);
775
- for (let i = 0; i < vec.length; i++) sum[i] += vec[i];
776
- }
777
- const avg = new Float32Array(sum.length);
778
- for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / rows.length;
779
- return avg;
780
- }
781
- function getAllFileEmbeddings(db, projectId) {
782
- const rows = db.prepare(`SELECT path, embedding FROM memory_chunks
783
- WHERE project_id = ? AND embedding IS NOT NULL
784
- ORDER BY path, start_line
785
- LIMIT ?`).all(projectId, MAX_CHUNKS);
786
- const byPath = /* @__PURE__ */ new Map();
787
- for (const row of rows) {
788
- const vec = deserializeEmbedding(row.embedding);
789
- const entry = byPath.get(row.path);
790
- if (!entry) byPath.set(row.path, {
791
- sum: new Float32Array(vec),
792
- count: 1
793
- });
794
- else {
795
- for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
796
- entry.count++;
797
- }
798
- }
799
- const result = /* @__PURE__ */ new Map();
800
- for (const [path, { sum, count }] of byPath) {
801
- const avg = new Float32Array(sum.length);
802
- for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
803
- result.set(path, avg);
804
- }
805
- return result;
806
- }
807
- function getFileTags(db, projectId, path) {
808
- return extractTagsFromChunkTexts(db.prepare(`SELECT text FROM memory_chunks
809
- WHERE project_id = ? AND path = ?
810
- ORDER BY start_line
811
- LIMIT 5`).all(projectId, path).map((r) => r.text));
812
- }
813
- function jaccardSimilarity(a, b) {
814
- if (a.size === 0 && b.size === 0) return 0;
815
- let intersection = 0;
816
- for (const tag of a) if (b.has(tag)) intersection++;
817
- const union = a.size + b.size - intersection;
818
- return union === 0 ? 0 : intersection / union;
819
- }
820
- function buildReason(semanticScore, tagScore, neighborScore, neighborCount) {
821
- const signals = [
822
- {
823
- label: `Semantically similar (${semanticScore.toFixed(2)})`,
824
- value: semanticScore * SEMANTIC_WEIGHT
825
- },
826
- {
827
- label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`,
828
- value: tagScore * TAG_WEIGHT
829
- },
830
- {
831
- label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? "s" : ""}`,
832
- value: neighborScore * NEIGHBOR_WEIGHT
833
- }
834
- ];
835
- signals.sort((a, b) => b.value - a.value);
836
- return signals[0].label;
837
- }
838
- function suggestedWikilink(vaultPath) {
839
- const base = basename(vaultPath);
840
- return `[[${base.endsWith(".md") ? base.slice(0, -3) : base}]]`;
841
- }
842
- /**
843
- * Proactively find notes worth linking to a given note, combining semantic similarity,
844
- * shared tags, and graph-neighborhood signals into a ranked list of suggestions.
845
- */
846
- async function zettelSuggest(db, opts) {
847
- const limit = opts.limit ?? 5;
848
- const excludeLinked = opts.excludeLinked ?? true;
849
- const outboundRows = db.prepare(`SELECT target_path FROM vault_links
850
- WHERE source_path = ? AND target_path IS NOT NULL`).all(opts.notePath);
851
- const linkedPaths = new Set(outboundRows.map((r) => r.target_path));
852
- const sourceEmbedding = getFileAvgEmbedding(db, opts.vaultProjectId, opts.notePath);
853
- const allEmbeddings = getAllFileEmbeddings(db, opts.vaultProjectId);
854
- allEmbeddings.delete(opts.notePath);
855
- const sourceTags = getFileTags(db, opts.vaultProjectId, opts.notePath);
856
- const friendTargetRows = db.prepare(`SELECT DISTINCT target_path AS path FROM vault_links
857
- WHERE source_path IN (
858
- SELECT target_path FROM vault_links
859
- WHERE source_path = ? AND target_path IS NOT NULL
860
- ) AND target_path IS NOT NULL`).all(opts.notePath);
861
- const friendLinkCounts = /* @__PURE__ */ new Map();
862
- for (const { path } of friendTargetRows) {
863
- if (path === opts.notePath) continue;
864
- friendLinkCounts.set(path, (friendLinkCounts.get(path) ?? 0) + 1);
865
- }
866
- const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());
867
- const allFiles = db.prepare("SELECT vault_path, title FROM vault_files").all();
868
- const suggestions = [];
869
- for (const { vault_path, title } of allFiles) {
870
- if (vault_path === opts.notePath) continue;
871
- if (excludeLinked && linkedPaths.has(vault_path)) continue;
872
- let semanticScore = 0;
873
- if (sourceEmbedding) {
874
- const candidateEmbedding = allEmbeddings.get(vault_path);
875
- if (candidateEmbedding) semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));
876
- }
877
- let tagScore = 0;
878
- if (allEmbeddings.has(vault_path)) tagScore = jaccardSimilarity(sourceTags, getFileTags(db, opts.vaultProjectId, vault_path));
879
- const friendCount = friendLinkCounts.get(vault_path) ?? 0;
880
- const neighborScore = friendCount / maxFriendLinks;
881
- const score = SEMANTIC_WEIGHT * semanticScore + TAG_WEIGHT * tagScore + NEIGHBOR_WEIGHT * neighborScore;
882
- if (score <= 0) continue;
883
- const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);
884
- suggestions.push({
885
- path: vault_path,
886
- title,
887
- score,
888
- semanticScore,
889
- tagScore,
890
- neighborScore,
891
- reason,
892
- suggestedWikilink: suggestedWikilink(vault_path)
893
- });
894
- }
895
- suggestions.sort((a, b) => b.score - a.score);
896
- return suggestions.slice(0, limit);
897
- }
898
-
899
- //#endregion
900
- export { zettelConverse, zettelExplore, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
901
- //# sourceMappingURL=zettelkasten-e-a4rW_6.mjs.map