@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.
- package/ARCHITECTURE.md +72 -1
- package/README.md +87 -1
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1482 -1628
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +11 -8
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-2ND5WO2j.mjs → daemon-D3hYb5_C.mjs} +669 -218
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +2 -2
- package/dist/hooks/capture-all-events.mjs.map +3 -3
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +93 -104
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +14 -11
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +2 -2
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +90 -91
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +94 -107
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +30 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -8
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-k-kUlaZ-.mjs → vault-indexer-Bi2cRmn7.mjs} +134 -132
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -1018
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-2ND5WO2j.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-k-kUlaZ-.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/CORE/Aesthetic.md +0 -333
- package/templates/skills/CORE/CONSTITUTION.md +0 -1502
- package/templates/skills/CORE/HistorySystem.md +0 -427
- package/templates/skills/CORE/HookSystem.md +0 -1082
- package/templates/skills/CORE/Prompting.md +0 -509
- package/templates/skills/CORE/ProsodyAgentTemplate.md +0 -53
- package/templates/skills/CORE/ProsodyGuide.md +0 -416
- package/templates/skills/CORE/SKILL.md +0 -741
- package/templates/skills/CORE/SkillSystem.md +0 -213
- package/templates/skills/CORE/TerminalTabs.md +0 -119
- package/templates/skills/CORE/VOICE.md +0 -106
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { a as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DGRAPAYb.mjs";
|
|
2
|
+
import { t as zettelThemes } from "./themes-BvYF0W8T.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
|
+
async function resolveStart(backend, startNote) {
|
|
10
|
+
const files = await backend.getVaultFilesByPaths([startNote]);
|
|
11
|
+
if (files.length > 0) return files[0].vaultPath;
|
|
12
|
+
const alias = await backend.getVaultAlias(startNote);
|
|
13
|
+
if (!alias) return null;
|
|
14
|
+
const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);
|
|
15
|
+
return canonical.length > 0 ? canonical[0].vaultPath : null;
|
|
16
|
+
}
|
|
17
|
+
async function getForwardNeighbors(backend, path) {
|
|
18
|
+
return (await backend.getLinksFromSource(path)).filter((l) => l.targetPath !== null).map((l) => l.targetPath);
|
|
19
|
+
}
|
|
20
|
+
async function getBackwardNeighbors(backend, path) {
|
|
21
|
+
return (await backend.getLinksToTarget(path)).map((l) => l.sourcePath);
|
|
22
|
+
}
|
|
23
|
+
async function getFileInfo(backend, path) {
|
|
24
|
+
const [files, health] = await Promise.all([backend.getVaultFilesByPaths([path]), backend.getVaultHealth(path)]);
|
|
25
|
+
return {
|
|
26
|
+
title: files[0]?.title ?? null,
|
|
27
|
+
inbound: health?.inboundCount ?? 0,
|
|
28
|
+
outbound: health?.outboundCount ?? 0
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Traverse the Zettelkasten link graph using BFS, following chains of thought
|
|
33
|
+
* from a starting note up to a configurable depth.
|
|
34
|
+
*/
|
|
35
|
+
async function zettelExplore(backend, opts) {
|
|
36
|
+
const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);
|
|
37
|
+
const direction = opts.direction ?? "both";
|
|
38
|
+
const mode = opts.mode ?? "all";
|
|
39
|
+
const root = await resolveStart(backend, opts.startNote);
|
|
40
|
+
if (!root) return {
|
|
41
|
+
root: opts.startNote,
|
|
42
|
+
nodes: [],
|
|
43
|
+
edges: [],
|
|
44
|
+
branchingPoints: [],
|
|
45
|
+
maxDepthReached: false
|
|
46
|
+
};
|
|
47
|
+
const visited = new Set([root]);
|
|
48
|
+
const nodes = [];
|
|
49
|
+
const edges = [];
|
|
50
|
+
let maxDepthReached = false;
|
|
51
|
+
const queue = [{
|
|
52
|
+
path: root,
|
|
53
|
+
depth: 0
|
|
54
|
+
}];
|
|
55
|
+
while (queue.length > 0) {
|
|
56
|
+
const current = queue.shift();
|
|
57
|
+
if (current.depth >= depth) {
|
|
58
|
+
maxDepthReached = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const neighbors = [];
|
|
62
|
+
if (direction === "forward" || direction === "both") for (const n of await getForwardNeighbors(backend, current.path)) neighbors.push({
|
|
63
|
+
neighbor: n,
|
|
64
|
+
from: current.path,
|
|
65
|
+
to: n
|
|
66
|
+
});
|
|
67
|
+
if (direction === "backward" || direction === "both") for (const n of await getBackwardNeighbors(backend, current.path)) neighbors.push({
|
|
68
|
+
neighbor: n,
|
|
69
|
+
from: n,
|
|
70
|
+
to: current.path
|
|
71
|
+
});
|
|
72
|
+
for (const { neighbor, from, to } of neighbors) {
|
|
73
|
+
const edgeType = classifyEdge(from, to);
|
|
74
|
+
if (mode !== "all" && edgeType !== mode) continue;
|
|
75
|
+
if (!edges.some((e) => e.from === from && e.to === to)) edges.push({
|
|
76
|
+
from,
|
|
77
|
+
to,
|
|
78
|
+
type: edgeType
|
|
79
|
+
});
|
|
80
|
+
if (!visited.has(neighbor)) {
|
|
81
|
+
visited.add(neighbor);
|
|
82
|
+
const info = await getFileInfo(backend, neighbor);
|
|
83
|
+
nodes.push({
|
|
84
|
+
path: neighbor,
|
|
85
|
+
title: info.title,
|
|
86
|
+
depth: current.depth + 1,
|
|
87
|
+
linkType: edgeType,
|
|
88
|
+
inbound: info.inbound,
|
|
89
|
+
outbound: info.outbound
|
|
90
|
+
});
|
|
91
|
+
queue.push({
|
|
92
|
+
path: neighbor,
|
|
93
|
+
depth: current.depth + 1
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const branchingPoints = nodes.filter((n) => n.outbound > 2).map((n) => n.path);
|
|
99
|
+
if ((await getFileInfo(backend, root)).outbound > 2) branchingPoints.unshift(root);
|
|
100
|
+
return {
|
|
101
|
+
root,
|
|
102
|
+
nodes,
|
|
103
|
+
edges,
|
|
104
|
+
branchingPoints,
|
|
105
|
+
maxDepthReached
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/zettelkasten/surprise.ts
|
|
111
|
+
const MAX_CHUNKS$1 = 5e3;
|
|
112
|
+
const BFS_HOP_CAP = 20;
|
|
113
|
+
async function getFileEmbeddings(backend, projectId) {
|
|
114
|
+
const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS$1);
|
|
115
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
const vec = deserializeEmbedding(row.embedding);
|
|
118
|
+
const entry = byPath.get(row.path);
|
|
119
|
+
if (!entry) byPath.set(row.path, {
|
|
120
|
+
sum: new Float32Array(vec),
|
|
121
|
+
count: 1,
|
|
122
|
+
text: row.text
|
|
123
|
+
});
|
|
124
|
+
else {
|
|
125
|
+
for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
|
|
126
|
+
entry.count++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const [path, { sum, count, text }] of byPath) {
|
|
131
|
+
const avg = new Float32Array(sum.length);
|
|
132
|
+
for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
|
|
133
|
+
result.set(path, {
|
|
134
|
+
embedding: avg,
|
|
135
|
+
text
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async function getReferenceEmbedding(backend, projectId, path) {
|
|
141
|
+
const rows = await backend.getChunksForPath(projectId, path);
|
|
142
|
+
if (rows.length === 0) return {
|
|
143
|
+
embedding: new Float32Array(0),
|
|
144
|
+
found: false
|
|
145
|
+
};
|
|
146
|
+
const embRows = rows.filter((r) => r.embedding !== null);
|
|
147
|
+
if (embRows.length === 0) return {
|
|
148
|
+
embedding: new Float32Array(0),
|
|
149
|
+
found: false
|
|
150
|
+
};
|
|
151
|
+
const dim = deserializeEmbedding(embRows[0].embedding).length;
|
|
152
|
+
const sum = new Float32Array(dim);
|
|
153
|
+
for (const row of embRows) {
|
|
154
|
+
const vec = deserializeEmbedding(row.embedding);
|
|
155
|
+
for (let i = 0; i < dim; i++) sum[i] += vec[i];
|
|
156
|
+
}
|
|
157
|
+
const avg = new Float32Array(dim);
|
|
158
|
+
for (let i = 0; i < dim; i++) avg[i] = sum[i] / embRows.length;
|
|
159
|
+
return {
|
|
160
|
+
embedding: avg,
|
|
161
|
+
found: true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function bfsGraphDistance(backend, source, target) {
|
|
165
|
+
if (source === target) return 0;
|
|
166
|
+
const visited = new Set([source]);
|
|
167
|
+
const queue = [{
|
|
168
|
+
path: source,
|
|
169
|
+
hops: 0
|
|
170
|
+
}];
|
|
171
|
+
while (queue.length > 0) {
|
|
172
|
+
const { path, hops } = queue.shift();
|
|
173
|
+
if (hops >= BFS_HOP_CAP) continue;
|
|
174
|
+
const [forwardLinks, backwardLinks] = await Promise.all([backend.getLinksFromSource(path), backend.getLinksToTarget(path)]);
|
|
175
|
+
const neighbors = [...forwardLinks.filter((l) => l.targetPath !== null).map((l) => l.targetPath), ...backwardLinks.map((l) => l.sourcePath)];
|
|
176
|
+
for (const neighbor of neighbors) {
|
|
177
|
+
if (neighbor === target) return hops + 1;
|
|
178
|
+
if (!visited.has(neighbor)) {
|
|
179
|
+
visited.add(neighbor);
|
|
180
|
+
queue.push({
|
|
181
|
+
path: neighbor,
|
|
182
|
+
hops: hops + 1
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Infinity;
|
|
188
|
+
}
|
|
189
|
+
function getBestChunkText(chunkRows, refEmbedding) {
|
|
190
|
+
const rows = chunkRows.filter((r) => r.embedding !== null);
|
|
191
|
+
if (rows.length === 0) return "";
|
|
192
|
+
let bestText = rows[0].text;
|
|
193
|
+
let bestSim = -Infinity;
|
|
194
|
+
for (const row of rows) {
|
|
195
|
+
const sim = cosineSimilarity(refEmbedding, deserializeEmbedding(row.embedding));
|
|
196
|
+
if (sim > bestSim) {
|
|
197
|
+
bestSim = sim;
|
|
198
|
+
bestText = row.text;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return bestText.trim().slice(0, 200);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Find notes that are semantically similar to a reference note but graph-distant —
|
|
205
|
+
* revealing surprising conceptual connections across unrelated areas of the Zettelkasten.
|
|
206
|
+
*/
|
|
207
|
+
async function zettelSurprise(backend, opts) {
|
|
208
|
+
const limit = opts.limit ?? 10;
|
|
209
|
+
const minSimilarity = opts.minSimilarity ?? .3;
|
|
210
|
+
const minGraphDistance = opts.minGraphDistance ?? 3;
|
|
211
|
+
let { embedding: refEmbedding, found } = await getReferenceEmbedding(backend, opts.vaultProjectId, opts.referencePath);
|
|
212
|
+
if (!found) refEmbedding = await generateEmbedding((await backend.getVaultFilesByPaths([opts.referencePath]))[0]?.title ?? opts.referencePath, true);
|
|
213
|
+
const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);
|
|
214
|
+
allFileEmbeddings.delete(opts.referencePath);
|
|
215
|
+
const semanticCandidates = [];
|
|
216
|
+
for (const [path, { embedding }] of allFileEmbeddings) {
|
|
217
|
+
const sim = cosineSimilarity(refEmbedding, embedding);
|
|
218
|
+
if (sim >= minSimilarity) semanticCandidates.push({
|
|
219
|
+
path,
|
|
220
|
+
sim
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const results = [];
|
|
224
|
+
for (const { path, sim } of semanticCandidates) {
|
|
225
|
+
const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);
|
|
226
|
+
const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;
|
|
227
|
+
if (effectiveDistance < minGraphDistance) continue;
|
|
228
|
+
const files = await backend.getVaultFilesByPaths([path]);
|
|
229
|
+
const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);
|
|
230
|
+
const surpriseScore = sim * Math.log2(effectiveDistance + 1);
|
|
231
|
+
const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);
|
|
232
|
+
results.push({
|
|
233
|
+
path,
|
|
234
|
+
title: files[0]?.title ?? null,
|
|
235
|
+
cosineSimilarity: sim,
|
|
236
|
+
graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,
|
|
237
|
+
surpriseScore,
|
|
238
|
+
sharedSnippet
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
results.sort((a, b) => b.surpriseScore - a.surpriseScore);
|
|
242
|
+
return results.slice(0, limit);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/zettelkasten/converse.ts
|
|
247
|
+
/** Extract the top-level folder from a vault path (first path segment). */
|
|
248
|
+
function extractDomain(vaultPath) {
|
|
249
|
+
const slash = vaultPath.indexOf("/");
|
|
250
|
+
return slash === -1 ? vaultPath : vaultPath.slice(0, slash);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Expand one level of graph neighbors for a set of paths.
|
|
254
|
+
* Returns all outbound and inbound neighbor paths (excluding already-visited).
|
|
255
|
+
*/
|
|
256
|
+
async function expandNeighbors(backend, paths) {
|
|
257
|
+
if (paths.size === 0) return [];
|
|
258
|
+
const pathList = Array.from(paths);
|
|
259
|
+
const [forwardLinks, backwardLinks] = await Promise.all([backend.getVaultLinksFromPaths(pathList), Promise.all(pathList.map((p) => backend.getLinksToTarget(p)))]);
|
|
260
|
+
const neighbors = [];
|
|
261
|
+
for (const link of forwardLinks) if (link.targetPath) neighbors.push(link.targetPath);
|
|
262
|
+
for (const linkList of backwardLinks) for (const link of linkList) neighbors.push(link.sourcePath);
|
|
263
|
+
return neighbors;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Hybrid search combining keyword + semantic results using the StorageBackend.
|
|
267
|
+
*/
|
|
268
|
+
async function hybridSearch(backend, query, queryEmbedding, opts) {
|
|
269
|
+
const maxResults = opts.maxResults ?? 10;
|
|
270
|
+
const kw = .5;
|
|
271
|
+
const sw = .5;
|
|
272
|
+
const [keywordResults, semanticResults] = await Promise.all([backend.searchKeyword(query, {
|
|
273
|
+
...opts,
|
|
274
|
+
maxResults: 50
|
|
275
|
+
}), backend.searchSemantic(queryEmbedding, {
|
|
276
|
+
...opts,
|
|
277
|
+
maxResults: 50
|
|
278
|
+
})]);
|
|
279
|
+
if (keywordResults.length === 0 && semanticResults.length === 0) return [];
|
|
280
|
+
const keyFor = (r) => `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;
|
|
281
|
+
function minMaxNormalize(scores) {
|
|
282
|
+
const min = Math.min(...scores);
|
|
283
|
+
const range = Math.max(...scores) - min;
|
|
284
|
+
if (range === 0) return scores.map(() => 1);
|
|
285
|
+
return scores.map((s) => (s - min) / range);
|
|
286
|
+
}
|
|
287
|
+
const kwNorm = minMaxNormalize(keywordResults.map((r) => r.score));
|
|
288
|
+
const semNorm = minMaxNormalize(semanticResults.map((r) => r.score));
|
|
289
|
+
const combined = /* @__PURE__ */ new Map();
|
|
290
|
+
for (let i = 0; i < keywordResults.length; i++) {
|
|
291
|
+
const r = keywordResults[i];
|
|
292
|
+
const k = keyFor(r);
|
|
293
|
+
combined.set(k, {
|
|
294
|
+
...r,
|
|
295
|
+
combinedScore: kw * kwNorm[i]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
for (let i = 0; i < semanticResults.length; i++) {
|
|
299
|
+
const r = semanticResults[i];
|
|
300
|
+
const k = keyFor(r);
|
|
301
|
+
const existing = combined.get(k);
|
|
302
|
+
if (existing) existing.combinedScore += sw * semNorm[i];
|
|
303
|
+
else combined.set(k, {
|
|
304
|
+
...r,
|
|
305
|
+
combinedScore: sw * semNorm[i]
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return Array.from(combined.values()).sort((a, b) => b.combinedScore - a.combinedScore).slice(0, maxResults).map((r) => ({
|
|
309
|
+
...r,
|
|
310
|
+
score: r.combinedScore
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Let the vault "talk back" — find notes relevant to a question, expand
|
|
315
|
+
* through the link graph, identify cross-domain connections, and return a
|
|
316
|
+
* structured result including a synthesis prompt for an AI to generate insights.
|
|
317
|
+
*/
|
|
318
|
+
async function zettelConverse(backend, opts) {
|
|
319
|
+
const depth = Math.max(opts.depth ?? 2, 0);
|
|
320
|
+
const limit = Math.max(opts.limit ?? 15, 1);
|
|
321
|
+
const candidateLimit = 20;
|
|
322
|
+
const queryEmbedding = await generateEmbedding(opts.question, true);
|
|
323
|
+
const searchResults = await hybridSearch(backend, opts.question, queryEmbedding, {
|
|
324
|
+
projectIds: [opts.vaultProjectId],
|
|
325
|
+
maxResults: candidateLimit
|
|
326
|
+
});
|
|
327
|
+
const searchHits = /* @__PURE__ */ new Map();
|
|
328
|
+
for (const r of searchResults) {
|
|
329
|
+
const existing = searchHits.get(r.path);
|
|
330
|
+
if (!existing || r.score > existing.score) searchHits.set(r.path, {
|
|
331
|
+
score: r.score,
|
|
332
|
+
snippet: r.snippet
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const allPaths = new Set(searchHits.keys());
|
|
336
|
+
let frontier = new Set(searchHits.keys());
|
|
337
|
+
for (let d = 0; d < depth; d++) {
|
|
338
|
+
const neighbors = await expandNeighbors(backend, frontier);
|
|
339
|
+
const newFrontier = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const n of neighbors) if (!allPaths.has(n)) {
|
|
341
|
+
allPaths.add(n);
|
|
342
|
+
newFrontier.add(n);
|
|
343
|
+
}
|
|
344
|
+
if (newFrontier.size === 0) break;
|
|
345
|
+
frontier = newFrontier;
|
|
346
|
+
}
|
|
347
|
+
const searchRanked = Array.from(searchHits.entries()).sort((a, b) => b[1].score - a[1].score).map(([path, info]) => ({
|
|
348
|
+
path,
|
|
349
|
+
...info,
|
|
350
|
+
isSearchResult: true
|
|
351
|
+
}));
|
|
352
|
+
const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));
|
|
353
|
+
const neighborHealthRows = await Promise.all(neighborPaths.map((p) => backend.getVaultHealth(p)));
|
|
354
|
+
const neighborRanked = neighborPaths.map((path, idx) => ({
|
|
355
|
+
path,
|
|
356
|
+
score: 0,
|
|
357
|
+
snippet: "",
|
|
358
|
+
inbound: neighborHealthRows[idx]?.inboundCount ?? 0,
|
|
359
|
+
isSearchResult: false
|
|
360
|
+
})).sort((a, b) => b.inbound - a.inbound);
|
|
361
|
+
const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);
|
|
362
|
+
const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);
|
|
363
|
+
const selectedSearchPaths = searchRanked.slice(0, limit);
|
|
364
|
+
const selectedPaths = new Set([...selectedSearchPaths.map((r) => r.path), ...selectedNeighbors.map((r) => r.path)]);
|
|
365
|
+
const allSelectedPaths = Array.from(selectedPaths);
|
|
366
|
+
const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);
|
|
367
|
+
const titleMap = new Map(fileRows.map((f) => [f.vaultPath, f.title]));
|
|
368
|
+
const relevantNotes = [];
|
|
369
|
+
for (const r of selectedSearchPaths) {
|
|
370
|
+
if (!selectedPaths.has(r.path)) continue;
|
|
371
|
+
relevantNotes.push({
|
|
372
|
+
path: r.path,
|
|
373
|
+
title: titleMap.get(r.path) ?? null,
|
|
374
|
+
snippet: r.snippet,
|
|
375
|
+
score: r.score,
|
|
376
|
+
domain: extractDomain(r.path)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
for (const r of selectedNeighbors) relevantNotes.push({
|
|
380
|
+
path: r.path,
|
|
381
|
+
title: titleMap.get(r.path) ?? null,
|
|
382
|
+
snippet: r.snippet,
|
|
383
|
+
score: 0,
|
|
384
|
+
domain: extractDomain(r.path)
|
|
385
|
+
});
|
|
386
|
+
let connections = [];
|
|
387
|
+
if (selectedPaths.size > 0) {
|
|
388
|
+
const pathList = Array.from(selectedPaths);
|
|
389
|
+
const pathSet = new Set(pathList);
|
|
390
|
+
const linkRows = await backend.getVaultLinksFromPaths(pathList);
|
|
391
|
+
const edgeCounts = /* @__PURE__ */ new Map();
|
|
392
|
+
for (const link of linkRows) if (link.targetPath && pathSet.has(link.targetPath)) {
|
|
393
|
+
const key = `${link.sourcePath}|||${link.targetPath}`;
|
|
394
|
+
edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);
|
|
395
|
+
}
|
|
396
|
+
for (const [key, cnt] of edgeCounts) {
|
|
397
|
+
const [sourcePath, targetPath] = key.split("|||");
|
|
398
|
+
connections.push({
|
|
399
|
+
fromPath: sourcePath,
|
|
400
|
+
toPath: targetPath,
|
|
401
|
+
fromDomain: extractDomain(sourcePath),
|
|
402
|
+
toDomain: extractDomain(targetPath),
|
|
403
|
+
strength: cnt
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const domainSet = new Set(relevantNotes.map((n) => n.domain));
|
|
408
|
+
const domains = Array.from(domainSet).sort();
|
|
409
|
+
const crossDomainConnections = connections.filter((c) => c.fromDomain !== c.toDomain);
|
|
410
|
+
const notesSummary = relevantNotes.map((n, i) => {
|
|
411
|
+
const title = n.title ? `"${n.title}"` : "(untitled)";
|
|
412
|
+
const domain = n.domain;
|
|
413
|
+
const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : " [context]";
|
|
414
|
+
const snippet = n.snippet.trim().slice(0, 300);
|
|
415
|
+
return `${i + 1}. [${domain}] ${title}${scoreLabel}\n Path: ${n.path}\n "${snippet}"`;
|
|
416
|
+
}).join("\n\n");
|
|
417
|
+
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)";
|
|
418
|
+
const domainList = domains.join(", ");
|
|
419
|
+
return {
|
|
420
|
+
relevantNotes,
|
|
421
|
+
connections: crossDomainConnections,
|
|
422
|
+
domains,
|
|
423
|
+
synthesisPrompt: `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:
|
|
424
|
+
|
|
425
|
+
QUESTION: ${opts.question}
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
RELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):
|
|
430
|
+
|
|
431
|
+
${notesSummary}
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
CROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):
|
|
436
|
+
|
|
437
|
+
${connectionSummary}
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
SYNTHESIS TASK:
|
|
442
|
+
|
|
443
|
+
Based on these notes and the connections between them, please:
|
|
444
|
+
|
|
445
|
+
1. Identify the key insights that emerge in direct response to the question.
|
|
446
|
+
2. Highlight any unexpected connections between notes from different domains (${domainList}).
|
|
447
|
+
3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.
|
|
448
|
+
4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.
|
|
449
|
+
5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.
|
|
450
|
+
|
|
451
|
+
Think like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/zettelkasten/health.ts
|
|
457
|
+
function countComponents(nodes, edges) {
|
|
458
|
+
if (nodes.length === 0) return 0;
|
|
459
|
+
const parent = /* @__PURE__ */ new Map();
|
|
460
|
+
const rank = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const n of nodes) {
|
|
462
|
+
parent.set(n, n);
|
|
463
|
+
rank.set(n, 0);
|
|
464
|
+
}
|
|
465
|
+
function find(x) {
|
|
466
|
+
let root = x;
|
|
467
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
468
|
+
let current = x;
|
|
469
|
+
while (current !== root) {
|
|
470
|
+
const next = parent.get(current);
|
|
471
|
+
parent.set(current, root);
|
|
472
|
+
current = next;
|
|
473
|
+
}
|
|
474
|
+
return root;
|
|
475
|
+
}
|
|
476
|
+
function union(a, b) {
|
|
477
|
+
const ra = find(a);
|
|
478
|
+
const rb = find(b);
|
|
479
|
+
if (ra === rb) return;
|
|
480
|
+
const rankA = rank.get(ra) ?? 0;
|
|
481
|
+
const rankB = rank.get(rb) ?? 0;
|
|
482
|
+
if (rankA < rankB) parent.set(ra, rb);
|
|
483
|
+
else if (rankA > rankB) parent.set(rb, ra);
|
|
484
|
+
else {
|
|
485
|
+
parent.set(rb, ra);
|
|
486
|
+
rank.set(ra, rankA + 1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (const { source, target } of edges) if (parent.has(source) && parent.has(target)) union(source, target);
|
|
490
|
+
const roots = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const n of nodes) roots.add(find(n));
|
|
492
|
+
return roots.size;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Audit the structural health of the Zettelkasten vault using graph metrics.
|
|
496
|
+
*/
|
|
497
|
+
async function zettelHealth(backend, opts) {
|
|
498
|
+
const options = opts ?? {};
|
|
499
|
+
const scope = options.scope ?? "full";
|
|
500
|
+
const include = options.include ?? [
|
|
501
|
+
"dead_links",
|
|
502
|
+
"orphans",
|
|
503
|
+
"disconnected",
|
|
504
|
+
"low_connectivity"
|
|
505
|
+
];
|
|
506
|
+
const computedAt = Date.now();
|
|
507
|
+
let totalFiles = 0;
|
|
508
|
+
if (scope === "full") totalFiles = await backend.countVaultFiles();
|
|
509
|
+
else if (scope === "project") {
|
|
510
|
+
const prefix = options.projectPath ?? "";
|
|
511
|
+
totalFiles = await backend.countVaultFilesWithPrefix(prefix);
|
|
512
|
+
} else {
|
|
513
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
514
|
+
totalFiles = await backend.countVaultFilesAfter(cutoff);
|
|
515
|
+
}
|
|
516
|
+
let totalLinks = 0;
|
|
517
|
+
if (scope === "full") totalLinks = (await backend.getVaultLinkGraph()).length;
|
|
518
|
+
else if (scope === "project") {
|
|
519
|
+
const prefix = options.projectPath ?? "";
|
|
520
|
+
totalLinks = await backend.countVaultLinksWithPrefix(prefix);
|
|
521
|
+
} else {
|
|
522
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
523
|
+
totalLinks = await backend.countVaultLinksAfter(cutoff);
|
|
524
|
+
}
|
|
525
|
+
let deadLinks = [];
|
|
526
|
+
if (include.includes("dead_links")) if (scope === "full") deadLinks = await backend.getDeadLinksWithLineNumbers();
|
|
527
|
+
else if (scope === "project") {
|
|
528
|
+
const prefix = options.projectPath ?? "";
|
|
529
|
+
deadLinks = await backend.getDeadLinksWithPrefix(prefix);
|
|
530
|
+
} else {
|
|
531
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
532
|
+
deadLinks = await backend.getDeadLinksAfter(cutoff);
|
|
533
|
+
}
|
|
534
|
+
let orphans = [];
|
|
535
|
+
if (include.includes("orphans")) if (scope === "full") orphans = (await backend.getOrphans()).map((r) => r.vaultPath);
|
|
536
|
+
else if (scope === "project") {
|
|
537
|
+
const prefix = options.projectPath ?? "";
|
|
538
|
+
orphans = await backend.getOrphansWithPrefix(prefix);
|
|
539
|
+
} else {
|
|
540
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
541
|
+
orphans = await backend.getOrphansAfter(cutoff);
|
|
542
|
+
}
|
|
543
|
+
let disconnectedClusters = 1;
|
|
544
|
+
if (include.includes("disconnected")) {
|
|
545
|
+
let allNodes;
|
|
546
|
+
let allEdges;
|
|
547
|
+
if (scope === "full") [allNodes, allEdges] = await Promise.all([backend.getAllVaultFilePaths(), backend.getVaultLinkEdges()]);
|
|
548
|
+
else if (scope === "project") {
|
|
549
|
+
const prefix = options.projectPath ?? "";
|
|
550
|
+
[allNodes, allEdges] = await Promise.all([backend.getVaultFilePathsWithPrefix(prefix), backend.getVaultLinkEdgesWithPrefix(prefix)]);
|
|
551
|
+
} else {
|
|
552
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
553
|
+
[allNodes, allEdges] = await Promise.all([backend.getVaultFilePathsAfter(cutoff), backend.getVaultLinkEdgesAfter(cutoff)]);
|
|
554
|
+
}
|
|
555
|
+
disconnectedClusters = countComponents(allNodes, allEdges);
|
|
556
|
+
}
|
|
557
|
+
let lowConnectivity = [];
|
|
558
|
+
if (include.includes("low_connectivity")) if (scope === "full") lowConnectivity = await backend.getLowConnectivity();
|
|
559
|
+
else if (scope === "project") {
|
|
560
|
+
const prefix = options.projectPath ?? "";
|
|
561
|
+
lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);
|
|
562
|
+
} else {
|
|
563
|
+
const cutoff = computedAt - (options.recentDays ?? 30) * 864e5;
|
|
564
|
+
lowConnectivity = await backend.getLowConnectivityAfter(cutoff);
|
|
565
|
+
}
|
|
566
|
+
const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;
|
|
567
|
+
const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;
|
|
568
|
+
const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;
|
|
569
|
+
const healthScore = Math.round(100 * (1 - deadRatio) * (1 - orphanRatio * .5) * (1 - lowConnRatio * .3));
|
|
570
|
+
return {
|
|
571
|
+
totalFiles,
|
|
572
|
+
totalLinks,
|
|
573
|
+
deadLinks,
|
|
574
|
+
orphans,
|
|
575
|
+
disconnectedClusters,
|
|
576
|
+
lowConnectivity,
|
|
577
|
+
healthScore,
|
|
578
|
+
computedAt
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/zettelkasten/suggest.ts
|
|
584
|
+
const MAX_CHUNKS = 5e3;
|
|
585
|
+
const SEMANTIC_WEIGHT = .5;
|
|
586
|
+
const TAG_WEIGHT = .2;
|
|
587
|
+
const NEIGHBOR_WEIGHT = .3;
|
|
588
|
+
function extractTagsFromChunkTexts(texts) {
|
|
589
|
+
const tags = /* @__PURE__ */ new Set();
|
|
590
|
+
for (const text of texts) {
|
|
591
|
+
const match = text.match(/^tags:\s*\n((?:[ \t]*-[ \t]*.+\n?)*)/m);
|
|
592
|
+
if (!match) continue;
|
|
593
|
+
const lines = match[1].split("\n");
|
|
594
|
+
for (const line of lines) {
|
|
595
|
+
const tagMatch = line.match(/^[ \t]*-[ \t]*(.+)/);
|
|
596
|
+
if (tagMatch) {
|
|
597
|
+
const tag = tagMatch[1].trim().toLowerCase();
|
|
598
|
+
if (tag) tags.add(tag);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return tags;
|
|
603
|
+
}
|
|
604
|
+
function jaccardSimilarity(a, b) {
|
|
605
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
606
|
+
let intersection = 0;
|
|
607
|
+
for (const tag of a) if (b.has(tag)) intersection++;
|
|
608
|
+
const union = a.size + b.size - intersection;
|
|
609
|
+
return union === 0 ? 0 : intersection / union;
|
|
610
|
+
}
|
|
611
|
+
function buildReason(semanticScore, tagScore, neighborScore, neighborCount) {
|
|
612
|
+
const signals = [
|
|
613
|
+
{
|
|
614
|
+
label: `Semantically similar (${semanticScore.toFixed(2)})`,
|
|
615
|
+
value: semanticScore * SEMANTIC_WEIGHT
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`,
|
|
619
|
+
value: tagScore * TAG_WEIGHT
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? "s" : ""}`,
|
|
623
|
+
value: neighborScore * NEIGHBOR_WEIGHT
|
|
624
|
+
}
|
|
625
|
+
];
|
|
626
|
+
signals.sort((a, b) => b.value - a.value);
|
|
627
|
+
return signals[0].label;
|
|
628
|
+
}
|
|
629
|
+
function suggestedWikilink(vaultPath) {
|
|
630
|
+
const base = basename(vaultPath);
|
|
631
|
+
return `[[${base.endsWith(".md") ? base.slice(0, -3) : base}]]`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Proactively find notes worth linking to a given note, combining semantic similarity,
|
|
635
|
+
* shared tags, and graph-neighborhood signals into a ranked list of suggestions.
|
|
636
|
+
*/
|
|
637
|
+
async function zettelSuggest(backend, opts) {
|
|
638
|
+
const limit = opts.limit ?? 5;
|
|
639
|
+
const excludeLinked = opts.excludeLinked ?? true;
|
|
640
|
+
const outboundLinks = await backend.getLinksFromSource(opts.notePath);
|
|
641
|
+
const linkedPaths = new Set(outboundLinks.filter((l) => l.targetPath !== null).map((l) => l.targetPath));
|
|
642
|
+
const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);
|
|
643
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const row of chunkRows) {
|
|
645
|
+
const vec = deserializeEmbedding(row.embedding);
|
|
646
|
+
const entry = byPath.get(row.path);
|
|
647
|
+
if (!entry) byPath.set(row.path, {
|
|
648
|
+
sum: new Float32Array(vec),
|
|
649
|
+
count: 1
|
|
650
|
+
});
|
|
651
|
+
else {
|
|
652
|
+
for (let i = 0; i < vec.length; i++) entry.sum[i] += vec[i];
|
|
653
|
+
entry.count++;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const allEmbeddings = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const [path, { sum, count }] of byPath) {
|
|
658
|
+
const avg = new Float32Array(sum.length);
|
|
659
|
+
for (let i = 0; i < sum.length; i++) avg[i] = sum[i] / count;
|
|
660
|
+
allEmbeddings.set(path, avg);
|
|
661
|
+
}
|
|
662
|
+
allEmbeddings.delete(opts.notePath);
|
|
663
|
+
const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;
|
|
664
|
+
const sourceTags = extractTagsFromChunkTexts((await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5)).map((r) => r.text));
|
|
665
|
+
const directTargets = (await backend.getLinksFromSource(opts.notePath)).filter((l) => l.targetPath !== null).map((l) => l.targetPath);
|
|
666
|
+
const friendLinkCounts = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const target of directTargets) {
|
|
668
|
+
const friendLinks = await backend.getLinksFromSource(target);
|
|
669
|
+
for (const link of friendLinks) if (link.targetPath && link.targetPath !== opts.notePath) friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);
|
|
670
|
+
}
|
|
671
|
+
const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());
|
|
672
|
+
const allFiles = await backend.getAllVaultFiles();
|
|
673
|
+
const suggestions = [];
|
|
674
|
+
for (const fileRow of allFiles) {
|
|
675
|
+
const vault_path = fileRow.vaultPath;
|
|
676
|
+
const title = fileRow.title;
|
|
677
|
+
if (vault_path === opts.notePath) continue;
|
|
678
|
+
if (excludeLinked && linkedPaths.has(vault_path)) continue;
|
|
679
|
+
let semanticScore = 0;
|
|
680
|
+
if (sourceEmbedding) {
|
|
681
|
+
const candidateEmbedding = allEmbeddings.get(vault_path);
|
|
682
|
+
if (candidateEmbedding) semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));
|
|
683
|
+
}
|
|
684
|
+
let tagScore = 0;
|
|
685
|
+
if (allEmbeddings.has(vault_path)) tagScore = jaccardSimilarity(sourceTags, extractTagsFromChunkTexts((await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5)).map((r) => r.text)));
|
|
686
|
+
const friendCount = friendLinkCounts.get(vault_path) ?? 0;
|
|
687
|
+
const neighborScore = friendCount / maxFriendLinks;
|
|
688
|
+
const score = SEMANTIC_WEIGHT * semanticScore + TAG_WEIGHT * tagScore + NEIGHBOR_WEIGHT * neighborScore;
|
|
689
|
+
if (score <= 0) continue;
|
|
690
|
+
const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);
|
|
691
|
+
suggestions.push({
|
|
692
|
+
path: vault_path,
|
|
693
|
+
title,
|
|
694
|
+
score,
|
|
695
|
+
semanticScore,
|
|
696
|
+
tagScore,
|
|
697
|
+
neighborScore,
|
|
698
|
+
reason,
|
|
699
|
+
suggestedWikilink: suggestedWikilink(vault_path)
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
suggestions.sort((a, b) => b.score - a.score);
|
|
703
|
+
return suggestions.slice(0, limit);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
export { zettelConverse, zettelExplore, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
|
|
708
|
+
//# sourceMappingURL=zettelkasten-cdajbnPr.mjs.map
|