@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
|
@@ -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
|