@gyoungwe/research-suite 0.1.0 → 0.1.2

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 (27) hide show
  1. package/README.md +6 -9
  2. package/build-manifest.json +11 -0
  3. package/node_modules/@gyoungwe/pi-core/README.md +10 -0
  4. package/node_modules/@gyoungwe/pi-core/build-manifest.json +8 -0
  5. package/node_modules/@gyoungwe/pi-core/dist/index.mjs +17412 -0
  6. package/node_modules/@gyoungwe/pi-core/package.json +62 -0
  7. package/node_modules/@gyoungwe/pi-knowledge/README.md +27 -0
  8. package/node_modules/@gyoungwe/pi-knowledge/build-manifest.json +13 -0
  9. package/node_modules/@gyoungwe/pi-knowledge/dist/index.mjs +13103 -0
  10. package/node_modules/@gyoungwe/pi-knowledge/lib/knowledge/runtime/worker.mjs +1680 -0
  11. package/node_modules/@gyoungwe/pi-knowledge/package.json +48 -0
  12. package/node_modules/@gyoungwe/pi-knowledge/plugin.manifest.json +27 -0
  13. package/node_modules/@gyoungwe/pi-knowledge/skills/research-show-me/SKILL.md +18 -0
  14. package/node_modules/@gyoungwe/pi-knowledge/skills/research-vault/SKILL.md +143 -0
  15. package/node_modules/@gyoungwe/pi-research/README.md +16 -0
  16. package/node_modules/@gyoungwe/pi-research/build-manifest.json +8 -0
  17. package/node_modules/@gyoungwe/pi-research/dist/index.mjs +16404 -0
  18. package/node_modules/@gyoungwe/pi-research/package.json +58 -0
  19. package/node_modules/@gyoungwe/pi-research/skills/research-workflow/SKILL.md +15 -0
  20. package/node_modules/@gyoungwe/plugin-api/README.md +15 -0
  21. package/node_modules/@gyoungwe/plugin-api/dist/index.d.ts +146 -0
  22. package/node_modules/@gyoungwe/plugin-api/dist/index.d.ts.map +1 -0
  23. package/node_modules/@gyoungwe/plugin-api/dist/index.js +86 -0
  24. package/node_modules/@gyoungwe/plugin-api/dist/index.js.map +1 -0
  25. package/node_modules/@gyoungwe/plugin-api/package.json +35 -0
  26. package/package.json +44 -55
  27. package/plugin.manifest.json +34 -0
@@ -0,0 +1,1680 @@
1
+ // @ts-nocheck
2
+ // packages/knowledge/src/worker.ts
3
+ import { watch } from "node:fs";
4
+ import { mkdir, readdir } from "node:fs/promises";
5
+ import { dirname, join as join2 } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { setImmediate as yieldTurn } from "node:timers/promises";
8
+ import { parentPort, workerData } from "node:worker_threads";
9
+
10
+ // packages/knowledge/src/files.ts
11
+ import { createHash } from "node:crypto";
12
+ import { constants } from "node:fs";
13
+ import { lstat, open, realpath } from "node:fs/promises";
14
+ import { isAbsolute, join, relative, sep } from "node:path";
15
+ var MAX_NOTE_BYTES = 1024 * 1024;
16
+ var OMIT = /* @__PURE__ */ new Set([
17
+ "node_modules",
18
+ "dist",
19
+ "build",
20
+ "out",
21
+ "coverage",
22
+ "vendor",
23
+ "venv",
24
+ "__pycache__",
25
+ "Attachments",
26
+ "Templates",
27
+ "_template"
28
+ ]);
29
+ var RESERVED = ["Projects", "Library", "Wiki", "Attachments", "Templates", "Indexes", "Inbox"];
30
+ function allowedSegment(name) {
31
+ return !!name && !name.startsWith(".") && !OMIT.has(name) && !/^(?:secrets?|credentials?|id_rsa|id_ed25519)(?:[.\-_]|$)/i.test(name);
32
+ }
33
+ function validateNote(path) {
34
+ if (typeof path !== "string" || isAbsolute(path) || path.includes("\\") || !path.endsWith(".md") || !path.split("/").every(allowedSegment))
35
+ throw new Error("Expected an allowed Vault-relative Markdown path");
36
+ const first = path.split("/")[0] ?? "";
37
+ if (RESERVED.some((name) => name.toLowerCase() === first.toLowerCase() && name !== first))
38
+ throw new Error("Reserved Vault directories require canonical casing");
39
+ return path;
40
+ }
41
+ function noteScope(path) {
42
+ const parts = path.split("/");
43
+ return path.startsWith("Projects/") && parts.length > 2 ? parts[1] ?? "shared" : "shared";
44
+ }
45
+ function canRead(path, project) {
46
+ const scope = noteScope(path);
47
+ return scope === "shared" || scope === project;
48
+ }
49
+ function fileVersion(stat) {
50
+ return `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}:${stat.ino}`;
51
+ }
52
+ async function safeNotePath(vault, path) {
53
+ validateNote(path);
54
+ let full = vault;
55
+ for (const part of path.split("/")) {
56
+ full = join(full, part);
57
+ if ((await lstat(full)).isSymbolicLink()) throw new Error("Knowledge reads do not follow symlinks");
58
+ }
59
+ const actual = await realpath(full);
60
+ const rel = relative(vault, actual);
61
+ if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
62
+ throw new Error("Note is outside the bound Vault");
63
+ return actual;
64
+ }
65
+ async function inspectNote(vault, path) {
66
+ const full = await safeNotePath(vault, path);
67
+ const stat = await lstat(full);
68
+ if (!stat.isFile()) throw new Error("Expected a regular note");
69
+ return { full, stat, signature: fileVersion(stat) };
70
+ }
71
+ async function closeQuietly(handle) {
72
+ try {
73
+ await handle.close();
74
+ } catch {
75
+ }
76
+ }
77
+ async function readNoteFile(vault, path) {
78
+ const { full } = await inspectNote(vault, path);
79
+ const handle = await open(full, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
80
+ try {
81
+ const before = await handle.stat();
82
+ if (!before.isFile() || before.size > MAX_NOTE_BYTES)
83
+ throw new Error("Note exceeds the 1 MiB indexing limit");
84
+ const buffer = Buffer.alloc(Number(before.size) + 1);
85
+ let offset = 0;
86
+ while (offset < buffer.length) {
87
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
88
+ if (!bytesRead) break;
89
+ offset += bytesRead;
90
+ }
91
+ const after = await handle.stat();
92
+ const current = await inspectNote(vault, path);
93
+ if (fileVersion(before) !== fileVersion(after) || fileVersion(after) !== current.signature || offset !== before.size)
94
+ throw new Error("Note changed while reading; retry against its latest version");
95
+ const bytes = buffer.subarray(0, offset);
96
+ return {
97
+ path,
98
+ text: bytes.toString("utf8"),
99
+ hash: createHash("sha256").update(bytes).digest("hex"),
100
+ signature: fileVersion(after),
101
+ bytes: offset
102
+ };
103
+ } catch (error) {
104
+ await closeQuietly(handle);
105
+ throw error;
106
+ } finally {
107
+ await closeQuietly(handle);
108
+ }
109
+ }
110
+ function snippet(text, { startLine = 1, maxChars = 5e3 } = {}) {
111
+ const lines = text.split("\n");
112
+ const start = Math.max(0, Math.floor(startLine) - 1);
113
+ const selected = [];
114
+ let chars = 0;
115
+ for (let i = start; i < lines.length; i++) {
116
+ const line = lines[i] ?? "";
117
+ if (chars + line.length + 1 > maxChars) {
118
+ if (!selected.length) selected.push(line.slice(0, maxChars));
119
+ break;
120
+ }
121
+ selected.push(line);
122
+ chars += line.length + 1;
123
+ }
124
+ return {
125
+ text: selected.join("\n"),
126
+ startLine: start + 1,
127
+ endLine: start + selected.length,
128
+ totalLines: lines.length,
129
+ truncated: start > 0 || start + selected.length < lines.length || selected.length === 1 && (selected[0]?.length ?? 0) < (lines[start]?.length || 0)
130
+ };
131
+ }
132
+
133
+ // packages/knowledge/src/graph-retrieval.ts
134
+ function backlinkContext(body, target, max = 240) {
135
+ const bare = target.split("/").at(-1)?.replace(/\.md$/, "") || target;
136
+ const full = target.replace(/\.md$/, "");
137
+ for (const line of String(body || "").split(/\r?\n/)) {
138
+ for (const m of line.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g)) {
139
+ const t = (m[1] || "").trim().replace(/\.md$/, "");
140
+ if (t === full || t.toLowerCase() === bare.toLowerCase()) {
141
+ const s = line.trim();
142
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
143
+ }
144
+ }
145
+ }
146
+ return void 0;
147
+ }
148
+
149
+ // packages/knowledge/src/search-policy.ts
150
+ function tokenizeKnowledgeText(text) {
151
+ const words = String(text).normalize("NFKC").toLowerCase().match(/[a-z0-9][a-z0-9._+-]*|[\p{Script=Han}]+/gu) || [];
152
+ const output = [];
153
+ for (const word of words) {
154
+ if (/\p{Script=Han}/u.test(word)) {
155
+ const characters = [...word];
156
+ for (let index = 0; index < characters.length; index++) {
157
+ const character = characters[index];
158
+ if (!character) continue;
159
+ output.push(character);
160
+ const next = characters[index + 1];
161
+ if (next) output.push(character + next);
162
+ }
163
+ } else output.push(word);
164
+ }
165
+ return output;
166
+ }
167
+ function buildKnowledgeSearchExpression(query) {
168
+ if (typeof query !== "string" || !query.trim() || query.length > 2e3)
169
+ throw new Error("Query must contain 1\u20132000 characters");
170
+ const terms = [...new Set(tokenizeKnowledgeText(query))].slice(0, 32);
171
+ if (!terms.length) throw new Error("Query has no searchable terms");
172
+ return terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
173
+ }
174
+ function splitKnowledgeChunks(text, maxChars = 1200) {
175
+ if (!Number.isInteger(maxChars) || maxChars < 1 || maxChars > 8e3)
176
+ throw new Error("Semantic chunk size must be an integer between 1 and 8000");
177
+ const chunks = [];
178
+ for (let start = 0; start < text.length && chunks.length < 64; start += maxChars)
179
+ chunks.push(text.slice(start, start + maxChars));
180
+ return chunks;
181
+ }
182
+
183
+ // packages/knowledge/src/ingest-frontmatter.ts
184
+ var KNOWLEDGE_TYPES = [
185
+ "paper",
186
+ "method",
187
+ "software",
188
+ "dataset",
189
+ "concept",
190
+ "entity",
191
+ "idea",
192
+ "claim",
193
+ "evidence",
194
+ "decision",
195
+ "question",
196
+ "wiki"
197
+ ];
198
+ var RUN_RESULT_TYPES = ["run", "artifact", "explainer"];
199
+ var NOTE_TYPES = [...KNOWLEDGE_TYPES, ...RUN_RESULT_TYPES];
200
+ var MAX_FRONTMATTER_CHARS = 64 * 1024;
201
+ var MAX_DEPTH = 6;
202
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
203
+ function splitFrontmatter(text) {
204
+ const source = String(text ?? "").replace(/^\uFEFF/, "");
205
+ const open2 = source.match(/^---[ \t]*\r?\n/);
206
+ if (!open2) return { raw: null, body: source };
207
+ const start = open2[0].length;
208
+ const rest = source.slice(start);
209
+ const close = rest.match(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
210
+ if (!close || close.index === void 0) return { raw: null, body: source };
211
+ return { raw: rest.slice(0, close.index), body: rest.slice(close.index + close[0].length) };
212
+ }
213
+ function stripComment(text) {
214
+ let quote = null;
215
+ for (let i = 0; i < text.length; i++) {
216
+ const char = text[i];
217
+ if (quote) {
218
+ if (char === "\\" && quote === '"') i++;
219
+ else if (char === quote) quote = null;
220
+ } else if (char === '"' || char === "'") quote = char;
221
+ else if (char === "#" && (i === 0 || /\s/.test(text[i - 1] ?? ""))) return text.slice(0, i).trimEnd();
222
+ }
223
+ return text;
224
+ }
225
+ function splitFlow(inner) {
226
+ const items = [];
227
+ let current = "";
228
+ let quote = null;
229
+ let depth = 0;
230
+ for (let i = 0; i < inner.length; i++) {
231
+ const char = inner[i] ?? "";
232
+ if (quote) {
233
+ current += char;
234
+ if (char === "\\" && quote === '"') current += inner[++i] ?? "";
235
+ else if (char === quote) quote = null;
236
+ continue;
237
+ }
238
+ if (char === '"' || char === "'") quote = char;
239
+ else if (char === "[" || char === "{") depth++;
240
+ else if (char === "]" || char === "}") depth--;
241
+ if (char === "," && depth === 0) {
242
+ items.push(current.trim());
243
+ current = "";
244
+ } else current += char;
245
+ }
246
+ if (current.trim()) items.push(current.trim());
247
+ return items;
248
+ }
249
+ function parseScalar(raw) {
250
+ const text = raw.trim();
251
+ if (text === "" || text === "~" || text === "null" || text === "Null" || text === "NULL") return null;
252
+ if (text === "true" || text === "True") return true;
253
+ if (text === "false" || text === "False") return false;
254
+ if (text.startsWith('"')) {
255
+ if (text.endsWith('"') && text.length >= 2) {
256
+ try {
257
+ return JSON.parse(text);
258
+ } catch {
259
+ return text.slice(1, -1).replace(/\\"/g, '"');
260
+ }
261
+ }
262
+ throw new Error("Unterminated double-quoted string");
263
+ }
264
+ if (text.startsWith("'")) {
265
+ if (text.endsWith("'") && text.length >= 2) return text.slice(1, -1).replace(/''/g, "'");
266
+ throw new Error("Unterminated single-quoted string");
267
+ }
268
+ if (text.startsWith("[")) {
269
+ if (!text.endsWith("]")) throw new Error("Unterminated flow sequence");
270
+ const inner = text.slice(1, -1).trim();
271
+ return inner ? splitFlow(inner).map(parseScalar) : [];
272
+ }
273
+ if (text.startsWith("{")) {
274
+ if (!text.endsWith("}")) throw new Error("Unterminated flow mapping");
275
+ const inner = text.slice(1, -1).trim();
276
+ const out = {};
277
+ if (!inner) return out;
278
+ for (const pair of splitFlow(inner)) {
279
+ const match = pair.match(/^("[^"]*"|'[^']*'|[^:]+?)\s*:\s*(.*)$/);
280
+ if (!match) throw new Error(`Invalid flow mapping entry: ${pair}`);
281
+ const key = String(parseScalar(match[1] ?? ""));
282
+ if (!UNSAFE_KEYS.has(key)) out[key] = parseScalar(match[2] ?? "");
283
+ }
284
+ return out;
285
+ }
286
+ if (/^[&*!|>]/.test(text)) throw new Error(`Unsupported YAML syntax: ${text.slice(0, 20)}`);
287
+ if (/^-?\d+(?:\.\d+)?$/.test(text) && text.length <= 15) return Number(text);
288
+ return text;
289
+ }
290
+ var KEY_LINE = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^\s"'#\-[{][^:#]*?|-[^\s:#][^:#]*?)\s*:(?:\s+(.*)|\s*)$/;
291
+ function isListItem(line) {
292
+ return line.text === "-" || line.text.startsWith("- ");
293
+ }
294
+ var Parser = class {
295
+ constructor(lines) {
296
+ this.lines = lines;
297
+ }
298
+ index = 0;
299
+ parseDocument() {
300
+ if (!this.lines.length) return {};
301
+ const first = this.lines[0];
302
+ if (first.indent !== 0) throw new Error(`Line ${first.number}: top-level key must not be indented`);
303
+ if (isListItem(first)) throw new Error(`Line ${first.number}: frontmatter must be a mapping`);
304
+ const value = this.parseMap(0, 0);
305
+ if (this.index < this.lines.length) {
306
+ const line = this.lines[this.index];
307
+ throw new Error(`Line ${line.number}: unexpected indentation`);
308
+ }
309
+ return value;
310
+ }
311
+ peek() {
312
+ return this.lines[this.index];
313
+ }
314
+ parseBlock(indent, depth) {
315
+ if (depth > MAX_DEPTH) throw new Error("Frontmatter nesting is too deep");
316
+ const line = this.peek();
317
+ if (!line) return null;
318
+ return isListItem(line) ? this.parseList(line.indent, depth) : this.parseMap(indent, depth);
319
+ }
320
+ valueAfterKey(indent, depth, rest) {
321
+ if (rest !== void 0 && rest.trim() !== "") {
322
+ const value = stripComment(rest);
323
+ if (/^[|>][+-]?$/.test(value.trim())) return this.parseBlockScalar(indent, value.trim());
324
+ return parseScalar(value);
325
+ }
326
+ const next = this.peek();
327
+ if (!next) return null;
328
+ if (next.indent > indent) return this.parseBlock(next.indent, depth + 1);
329
+ if (next.indent === indent && isListItem(next)) return this.parseList(indent, depth + 1);
330
+ return null;
331
+ }
332
+ parseBlockScalar(indent, style) {
333
+ const collected = [];
334
+ let blockIndent = null;
335
+ while (this.index < this.lines.length) {
336
+ const line = this.lines[this.index];
337
+ if (line.indent <= indent) break;
338
+ blockIndent ??= line.indent;
339
+ collected.push(`${" ".repeat(Math.max(0, line.indent - blockIndent))}${line.text}`);
340
+ this.index++;
341
+ }
342
+ const joined = style.startsWith("|") ? collected.join("\n") : collected.join(" ");
343
+ return style.endsWith("-") ? joined : `${joined}
344
+ `;
345
+ }
346
+ parseMap(indent, depth) {
347
+ const out = {};
348
+ while (this.index < this.lines.length) {
349
+ const line = this.lines[this.index];
350
+ if (line.indent < indent) break;
351
+ if (line.indent > indent) throw new Error(`Line ${line.number}: unexpected indentation`);
352
+ if (isListItem(line)) break;
353
+ const match = line.text.match(KEY_LINE);
354
+ if (!match) throw new Error(`Line ${line.number}: expected "key: value"`);
355
+ const key = String(parseScalar(match[1] ?? "")).trim();
356
+ this.index++;
357
+ const value = this.valueAfterKey(indent, depth, match[2]);
358
+ if (UNSAFE_KEYS.has(key)) continue;
359
+ if (Object.hasOwn(out, key)) throw new Error(`Line ${line.number}: duplicate key "${key}"`);
360
+ out[key] = value;
361
+ }
362
+ return out;
363
+ }
364
+ parseList(indent, depth) {
365
+ const out = [];
366
+ while (this.index < this.lines.length) {
367
+ const line = this.lines[this.index];
368
+ if (line.indent !== indent || !isListItem(line)) {
369
+ if (line.indent > indent) throw new Error(`Line ${line.number}: unexpected indentation`);
370
+ break;
371
+ }
372
+ const rest = line.text === "-" ? "" : line.text.slice(2);
373
+ const offset = line.text === "-" ? 1 : 2 + (rest.length - rest.trimStart().length);
374
+ const content = rest.trim();
375
+ if (!content) {
376
+ this.index++;
377
+ const next = this.peek();
378
+ out.push(next && next.indent > indent ? this.parseBlock(next.indent, depth + 1) : null);
379
+ continue;
380
+ }
381
+ if (KEY_LINE.test(stripComment(content)) && !/^["'[{]/.test(content)) {
382
+ this.lines[this.index] = { indent: indent + offset, text: content, number: line.number };
383
+ out.push(this.parseMap(indent + offset, depth + 1));
384
+ continue;
385
+ }
386
+ this.index++;
387
+ out.push(parseScalar(stripComment(content)));
388
+ }
389
+ return out;
390
+ }
391
+ };
392
+ function toLines(raw) {
393
+ const lines = [];
394
+ const source = raw.replace(/\r\n?/g, "\n").split("\n");
395
+ for (let i = 0; i < source.length; i++) {
396
+ const text = source[i] ?? "";
397
+ if (!text.trim() || /^\s*#/.test(text)) continue;
398
+ const leading = text.match(/^[ \t]*/)?.[0] ?? "";
399
+ if (leading.includes(" ")) throw new Error(`Line ${i + 2}: tabs are not allowed for indentation`);
400
+ lines.push({ indent: leading.length, text: text.slice(leading.length).trimEnd(), number: i + 2 });
401
+ }
402
+ return lines;
403
+ }
404
+ function parseFrontmatter(text) {
405
+ const { raw, body } = splitFrontmatter(text);
406
+ if (raw === null) return { data: null, body, errors: [] };
407
+ if (raw.length > MAX_FRONTMATTER_CHARS) return { data: null, body, errors: ["Frontmatter exceeds 64 KiB"] };
408
+ try {
409
+ return { data: new Parser(toLines(raw)).parseDocument(), body, errors: [] };
410
+ } catch (error) {
411
+ return { data: null, body, errors: [error instanceof Error ? error.message : String(error)] };
412
+ }
413
+ }
414
+
415
+ // packages/knowledge/src/semantic-model.ts
416
+ var WIDTH = 720;
417
+ var HEIGHT = 440;
418
+ var LAYOUT_MARGIN = 28;
419
+ var INFRASTRUCTURE = /(?:^|\/)(?:Index|Context|Home|Template|Templates)\.md$/i;
420
+ var MAIN_TYPES = /* @__PURE__ */ new Set(["question", "concept", "claim", "decision"]);
421
+ var CATEGORY_BY_PATH = [
422
+ [/\/Papers\//i, "paper"],
423
+ [/\/Methods?\//i, "method"],
424
+ [/\/Software\//i, "software"],
425
+ [/\/Datasets?\//i, "dataset"],
426
+ [/\/(?:Wiki|Wikis)\//i, "wiki"],
427
+ [/\/(?:Questions?|Research)\//i, "question"],
428
+ [/\/Memory\//i, "memory"]
429
+ ];
430
+ function contentCategory(node) {
431
+ const explicit = node.contentType?.trim().toLowerCase();
432
+ if (explicit) return explicit;
433
+ for (const [pattern, category] of CATEGORY_BY_PATH) if (pattern.test(node.path)) return category;
434
+ if (node.kind === "wiki") return "wiki";
435
+ return "other";
436
+ }
437
+ function isMainNode(node, category) {
438
+ if (MAIN_TYPES.has(category)) return true;
439
+ return /(?:^|\/)(?:Questions?|Research[-_ ]?Questions?)\/|(?:研究问题|研究主题|research question)/i.test(
440
+ `${node.path} ${node.title}`
441
+ );
442
+ }
443
+ var SECTOR_ANGLES = {
444
+ // SVG y grows downwards: 7 o'clock is 120° and 8 o'clock is 150°.
445
+ paper: 2 * Math.PI / 3,
446
+ software: 5 * Math.PI / 6,
447
+ method: Math.PI,
448
+ dataset: 7 * Math.PI / 6,
449
+ evidence: 4 * Math.PI / 3,
450
+ claim: 3 * Math.PI / 2,
451
+ concept: 11 * Math.PI / 6,
452
+ entity: 0,
453
+ idea: Math.PI / 6,
454
+ decision: Math.PI / 3,
455
+ question: Math.PI / 2,
456
+ wiki: 3 * Math.PI / 4,
457
+ other: 11 * Math.PI / 6
458
+ };
459
+ function sectorLayout(nodes, relations, seed) {
460
+ const paths = [...nodes.keys()].sort(comparePath);
461
+ const adjacency = new Map(paths.map((path) => [path, /* @__PURE__ */ new Set()]));
462
+ for (const relation of relations) {
463
+ if (relation.type !== "link") continue;
464
+ adjacency.get(relation.source)?.add(relation.target);
465
+ adjacency.get(relation.target)?.add(relation.source);
466
+ }
467
+ const categories = new Map(paths.map((path) => [path, contentCategory(nodes.get(path))]));
468
+ const mains = new Set(paths.filter((path) => isMainNode(nodes.get(path), categories.get(path) ?? "other")));
469
+ if (!mains.size) {
470
+ paths.slice().sort((a, b) => (adjacency.get(b)?.size ?? 0) - (adjacency.get(a)?.size ?? 0) || comparePath(a, b)).slice(0, Math.min(3, paths.length)).forEach((path) => {
471
+ mains.add(path);
472
+ });
473
+ }
474
+ const sharedWith = /* @__PURE__ */ new Map();
475
+ for (const path of paths) {
476
+ if (mains.has(path)) continue;
477
+ const connected = [...adjacency.get(path) ?? []].filter((candidate) => mains.has(candidate)).sort(comparePath);
478
+ if (connected.length > 1) sharedWith.set(path, connected);
479
+ }
480
+ const mainPaths = [...mains].sort(comparePath);
481
+ const centers = /* @__PURE__ */ new Map();
482
+ const centerY = HEIGHT * 0.48;
483
+ const columns = Math.max(1, Math.min(8, Math.ceil(Math.sqrt(mainPaths.length * WIDTH / HEIGHT))));
484
+ const rows = Math.ceil(mainPaths.length / columns);
485
+ const xStep = columns === 1 ? 0 : (WIDTH - LAYOUT_MARGIN * 2) / (columns - 1);
486
+ const yStep = rows === 1 ? 0 : (HEIGHT - LAYOUT_MARGIN * 2) / (rows - 1);
487
+ mainPaths.forEach((path, index) => {
488
+ const row = Math.floor(index / columns);
489
+ const column = index % columns;
490
+ const rowSize = Math.min(columns, mainPaths.length - row * columns);
491
+ const rowWidth = rowSize === 1 ? 0 : xStep * (rowSize - 1);
492
+ centers.set(path, {
493
+ x: columns === 1 ? WIDTH / 2 : WIDTH / 2 - rowWidth / 2 + column * xStep,
494
+ y: rows === 1 ? centerY : LAYOUT_MARGIN + row * yStep
495
+ });
496
+ });
497
+ const clampPoint = (point) => ({
498
+ x: Math.min(WIDTH - LAYOUT_MARGIN, Math.max(LAYOUT_MARGIN, point.x)),
499
+ y: Math.min(HEIGHT - LAYOUT_MARGIN, Math.max(LAYOUT_MARGIN, point.y))
500
+ });
501
+ const layout = {};
502
+ for (const path of mainPaths) layout[path] = clampPoint(centers.get(path));
503
+ const siblingGroups = /* @__PURE__ */ new Map();
504
+ for (const path of paths) {
505
+ if (mains.has(path) || sharedWith.has(path)) continue;
506
+ const connectedMain = [...adjacency.get(path) ?? []].find((candidate) => mains.has(candidate)) ?? "";
507
+ const category = categories.get(path) ?? "other";
508
+ const key = `${connectedMain}\0${category}`;
509
+ const group = siblingGroups.get(key) ?? [];
510
+ group.push(path);
511
+ siblingGroups.set(key, group);
512
+ }
513
+ for (const group of siblingGroups.values()) group.sort(comparePath);
514
+ for (const path of paths) {
515
+ if (mains.has(path)) continue;
516
+ const bridges = sharedWith.get(path);
517
+ if (bridges?.length) {
518
+ const average = bridges.reduce(
519
+ (point, main) => ({ x: point.x + (centers.get(main)?.x ?? WIDTH / 2), y: point.y + (centers.get(main)?.y ?? centerY) }),
520
+ { x: 0, y: 0 }
521
+ );
522
+ const jitter2 = (hash(`${path}:bridge`, seed) - 0.5) * 30;
523
+ layout[path] = clampPoint({ x: average.x / bridges.length, y: average.y / bridges.length - 36 + jitter2 });
524
+ continue;
525
+ }
526
+ const connectedMain = [...adjacency.get(path) ?? []].find((candidate) => mains.has(candidate));
527
+ const anchor = connectedMain ? centers.get(connectedMain) : { x: WIDTH / 2, y: centerY };
528
+ const category = categories.get(path) ?? "other";
529
+ const angle = SECTOR_ANGLES[category] ?? SECTOR_ANGLES.other ?? 0;
530
+ const siblings = siblingGroups.get(`${connectedMain ?? ""}\0${category}`) ?? [];
531
+ const index = Math.max(0, siblings.indexOf(path));
532
+ const spread = Math.min(0.22, 0.08 + siblings.length * 0.012);
533
+ const offset = (index - (siblings.length - 1) / 2) * spread;
534
+ const radius = 92 + Math.floor(index / 3) * 26 + hash(`${path}:radius`, seed) * 10;
535
+ const jitter = (hash(`${path}:angle`, seed) - 0.5) * 0.06;
536
+ layout[path] = clampPoint({
537
+ x: (anchor?.x ?? WIDTH / 2) + radius * Math.cos(angle + offset + jitter),
538
+ y: (anchor?.y ?? centerY) + radius * Math.sin(angle + offset + jitter)
539
+ });
540
+ }
541
+ for (const path of paths) {
542
+ if (layout[path]) continue;
543
+ const angle = 2 * Math.PI * hash(`${path}:outer`, seed);
544
+ layout[path] = clampPoint({ x: WIDTH / 2 + 175 * Math.cos(angle), y: centerY + 145 * Math.sin(angle) });
545
+ }
546
+ return { layout, mains, sharedWith };
547
+ }
548
+ function isInfrastructureNode(node) {
549
+ return node.kind === "navigation" || INFRASTRUCTURE.test(node.path);
550
+ }
551
+ function nodeType(node) {
552
+ if (isInfrastructureNode(node)) return "infrastructure";
553
+ if (contentCategory(node) === "memory") return "memory";
554
+ if (node.kind === "wiki") return "wiki";
555
+ return "note";
556
+ }
557
+ function classifyRelation(source, target, type) {
558
+ if (type) return type;
559
+ const sourceDir = source.path.slice(0, source.path.lastIndexOf("/"));
560
+ const targetDir = target.path.slice(0, target.path.lastIndexOf("/"));
561
+ return sourceDir && sourceDir === targetDir ? "directory" : "link";
562
+ }
563
+ function normalizeIdentity2(value) {
564
+ if (!value) return null;
565
+ const normalized = value.trim().toLowerCase().replace(/^https?:\/\/(?:dx\.)?doi\.org\//, "");
566
+ if (/^doi:\s*/.test(normalized) || /^10\.\d{4,9}\//.test(normalized)) return `doi:${normalized.replace(/^doi:\s*/, "")}`;
567
+ if (/^pmid:\s*\d+$/.test(normalized) || /^\d{1,9}$/.test(normalized)) return `pmid:${normalized.replace(/^pmid:\s*/, "")}`;
568
+ if (/^arxiv:\s*/.test(normalized) || /^\d{4}\.\d{4,5}(?:v\d+)?$/.test(normalized)) return `arxiv:${normalized.replace(/^arxiv:\s*/, "")}`;
569
+ return normalized || null;
570
+ }
571
+ function fallbackIdentity(node) {
572
+ if (node.identity) return normalizeIdentity2(node.identity);
573
+ return null;
574
+ }
575
+ function canonicalRank(path) {
576
+ return [path.startsWith("Library/") ? 0 : path.startsWith("Projects/") ? 1 : 2, path];
577
+ }
578
+ function comparePath(a, b) {
579
+ return a.localeCompare(b);
580
+ }
581
+ function hash(value, seed = 42) {
582
+ let h = (2166136261 ^ seed) >>> 0;
583
+ for (let i = 0; i < value.length; i++) h = Math.imul(h ^ value.charCodeAt(i), 16777619);
584
+ return (h >>> 0) / 4294967296;
585
+ }
586
+ function assignCommunities(paths, edges, seed = 42) {
587
+ const ordered = [...new Set(paths)].sort(comparePath);
588
+ const adjacency = new Map(ordered.map((path) => [path, /* @__PURE__ */ new Set()]));
589
+ for (const edge of edges) {
590
+ if (edge.type !== "link" || !adjacency.has(edge.source) || !adjacency.has(edge.target)) continue;
591
+ adjacency.get(edge.source)?.add(edge.target);
592
+ adjacency.get(edge.target)?.add(edge.source);
593
+ }
594
+ const labels = new Map(ordered.map((path, index) => [path, index]));
595
+ const degree = new Map(ordered.map((path) => [path, adjacency.get(path)?.size ?? 0]));
596
+ const total = [...degree.values()].reduce((sum, value) => sum + value, 0) || 1;
597
+ for (let round = 0; round < 12; round++) {
598
+ let changed2 = false;
599
+ for (const path of ordered) {
600
+ const neighbours = [...adjacency.get(path) ?? []].sort(comparePath);
601
+ if (!neighbours.length) continue;
602
+ const scores = /* @__PURE__ */ new Map();
603
+ for (const neighbour of neighbours) {
604
+ const label = labels.get(neighbour) ?? 0;
605
+ scores.set(label, (scores.get(label) ?? 0) + 1 - (degree.get(path) ?? 0) * (degree.get(neighbour) ?? 0) / total);
606
+ }
607
+ const current = labels.get(path) ?? 0;
608
+ const best = [...scores.entries()].sort((a, b) => b[1] - a[1] || hash(`${path}:${a[0]}`, seed) - hash(`${path}:${b[0]}`, seed) || a[0] - b[0])[0];
609
+ if (best && best[1] > 0 && best[0] !== current) {
610
+ labels.set(path, best[0]);
611
+ changed2 = true;
612
+ }
613
+ }
614
+ if (!changed2) break;
615
+ }
616
+ const counts = /* @__PURE__ */ new Map();
617
+ for (const label of labels.values()) counts.set(label, (counts.get(label) ?? 0) + 1);
618
+ const rank = new Map([...counts.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).map(([label], index) => [label, index]));
619
+ return Object.fromEntries(ordered.map((path) => [path, degree.get(path) ?? 0 ? rank.get(labels.get(path) ?? -1) ?? -1 : -1]));
620
+ }
621
+ function createSemanticModel(input, options = {}) {
622
+ const view = options.view ?? "semantic";
623
+ const mergeMirrors = options.mergeMirrors ?? true;
624
+ const seed = options.seed ?? 42;
625
+ const sourceNodes = [...input.nodes].sort((a, b) => comparePath(a.path, b.path));
626
+ const visible = sourceNodes.filter((node) => view === "all" || !isInfrastructureNode(node));
627
+ const groups = /* @__PURE__ */ new Map();
628
+ for (const node of visible) {
629
+ const identity = fallbackIdentity(node);
630
+ if (mergeMirrors && identity) {
631
+ const group = groups.get(identity) ?? [];
632
+ group.push(node);
633
+ groups.set(identity, group);
634
+ }
635
+ }
636
+ const canonicalFor = /* @__PURE__ */ new Map();
637
+ const duplicates = [];
638
+ for (const group of groups.values()) {
639
+ if (group.length < 2) continue;
640
+ const ordered = [...group].sort((a, b) => {
641
+ const [ar, ap] = canonicalRank(a.path);
642
+ const [br, bp] = canonicalRank(b.path);
643
+ return ar - br || ap.localeCompare(bp);
644
+ });
645
+ const first = ordered[0];
646
+ if (!first) continue;
647
+ const canonical = first.path;
648
+ for (const node of ordered) {
649
+ canonicalFor.set(node.path, canonical);
650
+ if (node.path !== canonical) duplicates.push({ canonical, duplicate: node.path });
651
+ }
652
+ }
653
+ const modelNodes = /* @__PURE__ */ new Map();
654
+ for (const node of visible) {
655
+ const canonical = canonicalFor.get(node.path) ?? node.path;
656
+ if (!modelNodes.has(canonical)) modelNodes.set(canonical, node);
657
+ }
658
+ const relations = [];
659
+ const relationKeys = /* @__PURE__ */ new Set();
660
+ const addRelation = (relation) => {
661
+ const key = `${relation.type}\0${relation.source}\0${relation.target}`;
662
+ if (relationKeys.has(key)) return;
663
+ relationKeys.add(key);
664
+ relations.push(relation);
665
+ };
666
+ for (const raw of input.edges ?? []) {
667
+ const source = canonicalFor.get(raw.source) ?? raw.source;
668
+ const target = canonicalFor.get(raw.target) ?? raw.target;
669
+ if (source === target || !modelNodes.has(source) || !modelNodes.has(target)) continue;
670
+ const type = classifyRelation({ path: source }, { path: target }, raw.type);
671
+ addRelation({ source, target, type });
672
+ }
673
+ if (view === "all") {
674
+ const all = [...modelNodes.keys()].sort(comparePath);
675
+ const siblings = /* @__PURE__ */ new Map();
676
+ for (const path of all) {
677
+ const parent = path.slice(0, path.lastIndexOf("/"));
678
+ const group = siblings.get(parent) ?? [];
679
+ group.push(path);
680
+ siblings.set(parent, group);
681
+ }
682
+ for (const group of siblings.values()) {
683
+ for (let i = 1; i < group.length; i++) {
684
+ const source = group[i - 1];
685
+ const target = group[i];
686
+ if (source && target) addRelation({ source, target, type: "directory" });
687
+ }
688
+ }
689
+ }
690
+ for (const duplicate of duplicates) addRelation({ source: duplicate.canonical, target: duplicate.duplicate, type: "duplicate" });
691
+ const communities = assignCommunities([...modelNodes.keys()], relations, seed);
692
+ const { layout, mains, sharedWith } = sectorLayout(modelNodes, relations, seed);
693
+ const linkRelations = relations.filter((edge) => edge.type === "link");
694
+ const degree = /* @__PURE__ */ new Map();
695
+ for (const edge of linkRelations) {
696
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
697
+ degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
698
+ }
699
+ const mirrorsByCanonical = /* @__PURE__ */ new Map();
700
+ for (const [mirror, canonical] of canonicalFor) {
701
+ if (mirror === canonical) continue;
702
+ const mirrors = mirrorsByCanonical.get(canonical) ?? [];
703
+ mirrors.push(mirror);
704
+ mirrorsByCanonical.set(canonical, mirrors);
705
+ }
706
+ for (const mirrors of mirrorsByCanonical.values()) mirrors.sort(comparePath);
707
+ const nodes = [...modelNodes.entries()].sort(([a], [b]) => comparePath(a, b)).map(([path, node]) => {
708
+ const mirrorPaths = mirrorsByCanonical.get(path) ?? [];
709
+ const type = nodeType(node);
710
+ const category = contentCategory(node);
711
+ return {
712
+ path,
713
+ title: node.title,
714
+ kind: node.kind,
715
+ degree: degree.get(path) ?? 0,
716
+ nodeType: mirrorPaths.length ? "mirror" : type,
717
+ isInfrastructure: type === "infrastructure",
718
+ community: communities[path] ?? -1,
719
+ contentType: category,
720
+ isMain: mains.has(path),
721
+ ...sharedWith.has(path) ? { sharedWith: sharedWith.get(path) } : {},
722
+ ...layout[path] ?? { x: WIDTH / 2, y: HEIGHT / 2 },
723
+ ...mirrorPaths.length ? { mirrors: mirrorPaths, warnings: ["mirror"] } : {}
724
+ };
725
+ });
726
+ return { nodes, edges: linkRelations.map(({ source, target }) => ({ source, target })), relations, duplicates, communities, layout };
727
+ }
728
+
729
+ // packages/knowledge/src/graph-cache.ts
730
+ function createRevisionCache() {
731
+ let activeRevision = null;
732
+ const values = /* @__PURE__ */ new Map();
733
+ const align = (revision2) => {
734
+ if (activeRevision === revision2) return;
735
+ activeRevision = revision2;
736
+ values.clear();
737
+ };
738
+ return {
739
+ get(revision2, key) {
740
+ align(revision2);
741
+ return values.get(key);
742
+ },
743
+ set(revision2, key, value) {
744
+ align(revision2);
745
+ values.set(key, value);
746
+ },
747
+ clear() {
748
+ activeRevision = null;
749
+ values.clear();
750
+ }
751
+ };
752
+ }
753
+
754
+ // packages/knowledge/src/worker.ts
755
+ await mkdir(dirname(workerData.database), { recursive: true, mode: 448 });
756
+ var db = new DatabaseSync(workerData.database, { timeout: 1500 });
757
+ var priorSchemaVersion = Number(db.prepare("PRAGMA user_version").get().user_version ?? 0);
758
+ if (!Number.isInteger(priorSchemaVersion) || priorSchemaVersion > 1)
759
+ throw new Error("Knowledge database schema is newer than this worker");
760
+ db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=1500; PRAGMA synchronous=NORMAL;
761
+ CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value INTEGER NOT NULL);
762
+ INSERT OR IGNORE INTO meta VALUES('revision',0);
763
+ CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE,
764
+ title TEXT NOT NULL, body TEXT NOT NULL, hash TEXT NOT NULL, signature TEXT NOT NULL,
765
+ scope TEXT NOT NULL, kind TEXT NOT NULL, bytes INTEGER NOT NULL);
766
+ CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(title, terms, tokenize='unicode61');
767
+ CREATE TABLE IF NOT EXISTS links(source TEXT NOT NULL, target TEXT NOT NULL, PRIMARY KEY(source,target));
768
+ CREATE INDEX IF NOT EXISTS links_target ON links(target);
769
+ CREATE INDEX IF NOT EXISTS notes_scope ON notes(scope,kind);
770
+ CREATE TABLE IF NOT EXISTS jobs(key TEXT PRIMARY KEY, kind TEXT NOT NULL, path TEXT NOT NULL,
771
+ scope TEXT NOT NULL, revision INTEGER NOT NULL, updated TEXT NOT NULL);
772
+ CREATE INDEX IF NOT EXISTS jobs_scope_updated ON jobs(scope,updated DESC);
773
+ CREATE INDEX IF NOT EXISTS jobs_scope_kind_updated ON jobs(scope,kind,updated DESC);
774
+ CREATE TABLE IF NOT EXISTS problems(path TEXT PRIMARY KEY, message TEXT NOT NULL);
775
+ CREATE TABLE IF NOT EXISTS semantic_chunks(
776
+ path TEXT NOT NULL, chunk_index INTEGER NOT NULL, hash TEXT NOT NULL, fingerprint TEXT NOT NULL,
777
+ mtime TEXT NOT NULL, vector TEXT NOT NULL, dimension INTEGER NOT NULL,
778
+ start_char INTEGER, end_char INTEGER,
779
+ PRIMARY KEY(path,chunk_index,hash,fingerprint)
780
+ );
781
+ CREATE INDEX IF NOT EXISTS semantic_chunks_lookup ON semantic_chunks(fingerprint,hash,path);
782
+ CREATE TABLE IF NOT EXISTS semantic_meta(fingerprint TEXT PRIMARY KEY, dimension INTEGER NOT NULL);
783
+ `);
784
+ if (priorSchemaVersion < 1) db.exec("PRAGMA user_version=1;");
785
+ var semanticColumns = db.prepare("PRAGMA table_info(semantic_chunks)").all().map((column) => column.name);
786
+ if (!semanticColumns.includes("start_char"))
787
+ db.exec("ALTER TABLE semantic_chunks ADD COLUMN start_char INTEGER");
788
+ if (!semanticColumns.includes("end_char")) db.exec("ALTER TABLE semantic_chunks ADD COLUMN end_char INTEGER");
789
+ if (semanticColumns.includes("text")) {
790
+ db.exec(`BEGIN IMMEDIATE;
791
+ DROP INDEX IF EXISTS semantic_chunks_lookup;
792
+ ALTER TABLE semantic_chunks RENAME TO semantic_chunks_legacy;
793
+ CREATE TABLE semantic_chunks(
794
+ path TEXT NOT NULL, chunk_index INTEGER NOT NULL, hash TEXT NOT NULL, fingerprint TEXT NOT NULL,
795
+ mtime TEXT NOT NULL, vector TEXT NOT NULL, dimension INTEGER NOT NULL,
796
+ start_char INTEGER, end_char INTEGER,
797
+ PRIMARY KEY(path,chunk_index,hash,fingerprint)
798
+ );
799
+ INSERT INTO semantic_chunks(path,chunk_index,hash,fingerprint,mtime,vector,dimension)
800
+ SELECT path,chunk_index,hash,fingerprint,mtime,vector,dimension FROM semantic_chunks_legacy;
801
+ DROP TABLE semantic_chunks_legacy;
802
+ CREATE INDEX IF NOT EXISTS semantic_chunks_lookup ON semantic_chunks(fingerprint,hash,path);
803
+ COMMIT;`);
804
+ }
805
+ var revision = Number(db.prepare("SELECT value FROM meta WHERE key='revision'").get().value);
806
+ var scan = null;
807
+ var coverage = "uninitialized";
808
+ var lastReconciledAt = null;
809
+ var watching = false;
810
+ var timer = null;
811
+ var _closed = false;
812
+ var fullScanRequested = false;
813
+ var activeRequests = /* @__PURE__ */ new Set();
814
+ var flushes = /* @__PURE__ */ new Set();
815
+ var closePromise;
816
+ var dirty = /* @__PURE__ */ new Set();
817
+ var inflight = /* @__PURE__ */ new Map();
818
+ var stats = { bodyReads: 0, metadataChecks: 0, reconciliations: 0, changes: 0, searches: 0 };
819
+ var graphSnapshots = createRevisionCache();
820
+ var statements = {
821
+ get: db.prepare("SELECT * FROM notes WHERE path=?"),
822
+ count: db.prepare("SELECT count(*) count FROM notes"),
823
+ removeFts: db.prepare("DELETE FROM search WHERE rowid=?"),
824
+ remove: db.prepare("DELETE FROM notes WHERE path=?"),
825
+ links: db.prepare("DELETE FROM links WHERE source=?"),
826
+ addLink: db.prepare("INSERT OR IGNORE INTO links VALUES(?,?)"),
827
+ problem: db.prepare("INSERT OR REPLACE INTO problems VALUES(?,?)"),
828
+ clearProblem: db.prepare("DELETE FROM problems WHERE path=?"),
829
+ semanticDelete: db.prepare("DELETE FROM semantic_chunks WHERE path=?"),
830
+ job: db.prepare(
831
+ "INSERT INTO jobs VALUES(?,?,?,?,?,?) ON CONFLICT(key) DO UPDATE SET revision=excluded.revision,updated=excluded.updated"
832
+ )
833
+ };
834
+ function bump() {
835
+ revision = Number(
836
+ db.prepare("UPDATE meta SET value=value+1 WHERE key='revision' RETURNING value").get().value
837
+ );
838
+ }
839
+ function job(kind, path, scope) {
840
+ statements.job.run(`${kind}:${path}`, kind, path, scope, revision, (/* @__PURE__ */ new Date()).toISOString());
841
+ }
842
+ var uiTimer = null;
843
+ function signalUiChange() {
844
+ if (_closed || uiTimer) return;
845
+ uiTimer = setTimeout(() => {
846
+ uiTimer = null;
847
+ parentPort.postMessage({ uiChanged: true });
848
+ }, 500);
849
+ uiTimer.unref();
850
+ }
851
+ function changed(path, previous) {
852
+ signalUiChange();
853
+ stats.changes++;
854
+ const scope = noteScope(path);
855
+ if (!/(?:^|\/)(?:Index|Context|Home)\.md$/i.test(path)) {
856
+ job(
857
+ "navigation",
858
+ scope === "shared" ? path.startsWith("Wiki/") ? "Wiki/Index.md" : "Library/Index.md" : `Projects/${scope}/Index.md`,
859
+ scope
860
+ );
861
+ if (!/(?:^|\/)Wiki\//.test(path)) job("evidence-review", path, scope);
862
+ }
863
+ const dependents = db.prepare("SELECT source FROM links WHERE target=?").all(path);
864
+ for (const { source } of dependents)
865
+ if (/(?:^|\/)Wiki\//.test(source) && !source.endsWith("/Index.md"))
866
+ job("wiki-review", source, noteScope(source));
867
+ if (previous && previous.scope !== scope)
868
+ job("navigation", `Projects/${previous.scope}/Index.md`, previous.scope);
869
+ }
870
+ function internalLinks(path, text) {
871
+ const out = /* @__PURE__ */ new Set();
872
+ for (const match of text.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]/g)) {
873
+ let target = match[1].trim();
874
+ if (!target.includes("/")) {
875
+ target = ["Home", "Index", "Context"].includes(target) && target !== "Home" ? `${path.split("/").slice(0, -1).join("/")}/${target}` : target;
876
+ }
877
+ if (!target.endsWith(".md")) target += ".md";
878
+ try {
879
+ validateNote(target);
880
+ out.add(target);
881
+ } catch {
882
+ }
883
+ }
884
+ return out;
885
+ }
886
+ async function updatePath(path, force = false) {
887
+ validateNote(path);
888
+ if (inflight.has(path)) return inflight.get(path);
889
+ const task = (async () => {
890
+ const previous = statements.get.get(path);
891
+ try {
892
+ stats.metadataChecks++;
893
+ const info = await inspectNote(workerData.vault, path);
894
+ if (!force && previous?.signature === info.signature) return previous;
895
+ const file = await readNoteFile(workerData.vault, path);
896
+ stats.bodyReads++;
897
+ if (previous?.hash === file.hash) {
898
+ db.prepare("UPDATE notes SET signature=? WHERE path=?").run(file.signature, path);
899
+ statements.clearProblem.run(path);
900
+ return statements.get.get(path);
901
+ }
902
+ const title = file.text.match(/^#\s+(.+)$/m)?.[1]?.trim() || path.split("/").at(-1).slice(0, -3);
903
+ const kind = /(?:^|\/)(?:Index|Context|Home)\.md$/i.test(path) ? "navigation" : /(?:^|\/)Wiki\//.test(path) ? "wiki" : path.startsWith("Library/Explainers/") ? "explainer" : "note";
904
+ db.exec("BEGIN IMMEDIATE");
905
+ try {
906
+ const current = statements.get.get(path);
907
+ if (current) statements.removeFts.run(current.id);
908
+ if (current && (current.hash !== file.hash || current.signature !== file.signature))
909
+ statements.semanticDelete.run(path);
910
+ db.prepare(`INSERT INTO notes(path,title,body,hash,signature,scope,kind,bytes) VALUES(?,?,?,?,?,?,?,?)
911
+ ON CONFLICT(path) DO UPDATE SET title=excluded.title,body=excluded.body,hash=excluded.hash,
912
+ signature=excluded.signature,scope=excluded.scope,kind=excluded.kind,bytes=excluded.bytes`).run(
913
+ path,
914
+ title,
915
+ file.text,
916
+ file.hash,
917
+ file.signature,
918
+ noteScope(path),
919
+ kind,
920
+ file.bytes
921
+ );
922
+ const row = statements.get.get(path);
923
+ statements.removeFts.run(row.id);
924
+ db.prepare("INSERT INTO search(rowid,title,terms) VALUES(?,?,?)").run(
925
+ row.id,
926
+ tokenizeKnowledgeText(title).join(" "),
927
+ tokenizeKnowledgeText(file.text).join(" ")
928
+ );
929
+ statements.links.run(path);
930
+ for (const target of internalLinks(path, file.text)) statements.addLink.run(path, target);
931
+ statements.clearProblem.run(path);
932
+ bump();
933
+ changed(path, previous);
934
+ db.exec("COMMIT");
935
+ return row;
936
+ } catch (error) {
937
+ db.exec("ROLLBACK");
938
+ revision = Number(db.prepare("SELECT value FROM meta WHERE key='revision'").get().value);
939
+ throw error;
940
+ }
941
+ } catch (error) {
942
+ if (previous) {
943
+ db.exec("BEGIN IMMEDIATE");
944
+ try {
945
+ const current = statements.get.get(path);
946
+ if (current) statements.removeFts.run(current.id);
947
+ statements.remove.run(path);
948
+ statements.semanticDelete.run(path);
949
+ statements.links.run(path);
950
+ bump();
951
+ changed(path, previous);
952
+ db.exec("COMMIT");
953
+ } catch (inner) {
954
+ db.exec("ROLLBACK");
955
+ throw inner;
956
+ }
957
+ }
958
+ if (["ENOENT", "ENOTDIR"].includes(error.code)) statements.clearProblem.run(path);
959
+ else statements.problem.run(path, String(error.message).slice(0, 400));
960
+ if (["ENOENT", "ENOTDIR"].includes(error.code)) return null;
961
+ throw error;
962
+ }
963
+ })();
964
+ inflight.set(path, task);
965
+ try {
966
+ return await task;
967
+ } finally {
968
+ if (inflight.get(path) === task) inflight.delete(path);
969
+ }
970
+ }
971
+ async function* walk(directory, prefix = "") {
972
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
973
+ if (!allowedSegment(entry.name) || entry.isSymbolicLink()) continue;
974
+ const path = prefix ? `${prefix}/${entry.name}` : entry.name;
975
+ if (entry.isDirectory()) yield* walk(join2(directory, entry.name), path);
976
+ else if (entry.isFile() && path.endsWith(".md")) yield path;
977
+ }
978
+ }
979
+ function beginReconcile(force = false) {
980
+ if (_closed) return Promise.resolve();
981
+ if (scan) return scan;
982
+ coverage = "indexing";
983
+ stats.reconciliations++;
984
+ scan = (async () => {
985
+ const seen = /* @__PURE__ */ new Set(), initial = db.prepare("SELECT path FROM notes").all();
986
+ let n = 0, interrupted = false;
987
+ try {
988
+ for await (const path of walk(workerData.vault)) {
989
+ seen.add(path);
990
+ try {
991
+ await updatePath(path, force);
992
+ } catch {
993
+ }
994
+ if (++n % 24 === 0) await yieldTurn();
995
+ }
996
+ } catch (error) {
997
+ interrupted = true;
998
+ statements.problem.run("<inventory>", String(error.message));
999
+ }
1000
+ if (!interrupted) {
1001
+ statements.clearProblem.run("<inventory>");
1002
+ for (const { path } of initial)
1003
+ if (!seen.has(path)) {
1004
+ try {
1005
+ await updatePath(path, true);
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ lastReconciledAt = (/* @__PURE__ */ new Date()).toISOString();
1010
+ }
1011
+ coverage = interrupted ? "partial" : "ready";
1012
+ signalUiChange();
1013
+ })().finally(() => {
1014
+ scan = null;
1015
+ });
1016
+ return scan;
1017
+ }
1018
+ function flushDirty(limit = 32) {
1019
+ if (_closed) return Promise.resolve();
1020
+ const task = flushDirtyBatch(limit);
1021
+ flushes.add(task);
1022
+ void task.then(
1023
+ () => flushes.delete(task),
1024
+ () => flushes.delete(task)
1025
+ );
1026
+ return task;
1027
+ }
1028
+ async function flushDirtyBatch(limit) {
1029
+ const paths = [...dirty].slice(0, limit);
1030
+ for (let i = 0; i < paths.length; i++) {
1031
+ dirty.delete(paths[i]);
1032
+ try {
1033
+ await updatePath(paths[i]);
1034
+ } catch {
1035
+ }
1036
+ if (i % 16 === 15) await yieldTurn();
1037
+ }
1038
+ if (dirty.size) schedule();
1039
+ if (fullScanRequested && !scan) {
1040
+ fullScanRequested = false;
1041
+ void beginReconcile();
1042
+ }
1043
+ }
1044
+ function schedule() {
1045
+ if (_closed || timer) return;
1046
+ timer = setTimeout(() => {
1047
+ timer = null;
1048
+ void flushDirty();
1049
+ }, 150);
1050
+ timer.unref();
1051
+ }
1052
+ var watcher;
1053
+ try {
1054
+ watcher = watch(workerData.vault, { recursive: true, persistent: false }, (_event, name) => {
1055
+ if (_closed) return;
1056
+ const path = name?.toString().replaceAll("\\", "/");
1057
+ if (path?.endsWith(".md")) {
1058
+ try {
1059
+ validateNote(path);
1060
+ dirty.add(path);
1061
+ } catch {
1062
+ return;
1063
+ }
1064
+ } else if (!path || path.split("/").every(allowedSegment)) fullScanRequested = true;
1065
+ schedule();
1066
+ });
1067
+ watcher.on("error", (error) => {
1068
+ if (_closed) return;
1069
+ watching = false;
1070
+ statements.problem.run("<watcher>", String(error.message));
1071
+ });
1072
+ watching = true;
1073
+ } catch (error) {
1074
+ statements.problem.run("<watcher>", String(error.message));
1075
+ }
1076
+ var reconcileTimer = setInterval(() => {
1077
+ if (!scan) void beginReconcile();
1078
+ }, workerData.reconcileMs || 12e4);
1079
+ reconcileTimer.unref();
1080
+ function closeWorker() {
1081
+ if (closePromise) return closePromise;
1082
+ _closed = true;
1083
+ if (timer) clearTimeout(timer);
1084
+ if (uiTimer) clearTimeout(uiTimer);
1085
+ clearInterval(reconcileTimer);
1086
+ watcher?.close();
1087
+ watching = false;
1088
+ fullScanRequested = false;
1089
+ closePromise = (async () => {
1090
+ await Promise.allSettled([...activeRequests, ...flushes, ...inflight.values(), scan]);
1091
+ dirty.clear();
1092
+ db.close();
1093
+ })();
1094
+ return closePromise;
1095
+ }
1096
+ function status() {
1097
+ revision = Number(db.prepare("SELECT value FROM meta WHERE key='revision'").get().value);
1098
+ return {
1099
+ revision,
1100
+ coverage,
1101
+ noteCount: Number(statements.count.get().count),
1102
+ lastReconciledAt,
1103
+ watching,
1104
+ pendingChanges: dirty.size,
1105
+ jobs: Number(db.prepare("SELECT count(*) count FROM jobs").get().count),
1106
+ problems: db.prepare("SELECT * FROM problems ORDER BY path LIMIT 20").all(),
1107
+ stats: { ...stats },
1108
+ semantic: {
1109
+ vectors: Number(db.prepare("SELECT count(*) count FROM semantic_chunks").get().count),
1110
+ paths: Number(db.prepare("SELECT count(DISTINCT path) count FROM semantic_chunks").get().count)
1111
+ }
1112
+ };
1113
+ }
1114
+ function semanticStatus(fingerprint = null, project = null) {
1115
+ if (!fingerprint) return { vectors: 0, paths: 0, coverage: "unconfigured" };
1116
+ const scope = project || "";
1117
+ const row = db.prepare(`SELECT count(*) vectors,count(DISTINCT s.path) paths
1118
+ FROM semantic_chunks s JOIN notes n ON n.path=s.path AND n.hash=s.hash
1119
+ WHERE s.fingerprint=? AND (n.scope='shared' OR n.scope=?) AND n.kind NOT IN ('navigation','explainer')`).get(fingerprint, scope);
1120
+ return { vectors: Number(row.vectors), paths: Number(row.paths), fingerprint, project: scope };
1121
+ }
1122
+ function pending(project, limit = 20, kind = null) {
1123
+ return db.prepare(
1124
+ "SELECT * FROM jobs WHERE (scope='shared' OR scope=?) AND (? IS NULL OR kind=?) ORDER BY updated DESC LIMIT ?"
1125
+ ).all(project || "", kind, kind, limit);
1126
+ }
1127
+ async function read(path, project, options = {}) {
1128
+ if (!canRead(validateNote(path), project)) throw new Error("Note is outside shared/current-project scope");
1129
+ const row = await updatePath(path);
1130
+ if (!row) return { path, missing: true };
1131
+ const part = snippet(row.body, options);
1132
+ const reviewAt = row.body.split("\n").findIndex((line) => /^## Human review\s*$/i.test(line));
1133
+ const humanReview = reviewAt >= 0 ? snippet(row.body, { startLine: reviewAt + 1, maxChars: options.reviewMaxChars || 1600 }) : null;
1134
+ return {
1135
+ path,
1136
+ hash: row.hash,
1137
+ ...part,
1138
+ humanReview,
1139
+ pending: db.prepare("SELECT kind,revision FROM jobs WHERE path=?").all(path)
1140
+ };
1141
+ }
1142
+ async function search(args) {
1143
+ stats.searches++;
1144
+ if (coverage === "uninitialized") void beginReconcile();
1145
+ await flushDirty();
1146
+ const expression = buildKnowledgeSearchExpression(args.query);
1147
+ const limit = Math.max(1, Math.min(12, Math.floor(args.limit || 5)));
1148
+ const kindFilter = args.wikiOnly ? "AND n.kind='wiki'" : args.explainerOnly ? "AND n.kind='explainer'" : "AND n.kind!='explainer'";
1149
+ const rows = db.prepare(`SELECT n.path,n.title,n.hash,n.kind,n.body,bm25(search,6.0,1.0) rank
1150
+ FROM search CROSS JOIN notes n ON n.id=search.rowid
1151
+ WHERE search MATCH ? AND (n.scope='shared' OR n.scope=?) ${kindFilter}
1152
+ ORDER BY rank,n.path LIMIT ?`).all(expression, args.project || "", limit * 2);
1153
+ const hits = [];
1154
+ let staleCandidates = 0;
1155
+ for (const item of rows) {
1156
+ if (hits.length >= limit) break;
1157
+ let current;
1158
+ try {
1159
+ current = await updatePath(item.path);
1160
+ } catch {
1161
+ continue;
1162
+ }
1163
+ if (!current || current.hash !== item.hash) {
1164
+ staleCandidates++;
1165
+ continue;
1166
+ }
1167
+ const lower = current.body.normalize("NFKC").toLowerCase();
1168
+ const needle = String(args.query).normalize("NFKC").toLowerCase();
1169
+ let at = lower.indexOf(needle);
1170
+ if (at < 0)
1171
+ at = tokenizeKnowledgeText(args.query).map((t) => lower.indexOf(t)).find((i) => i >= 0) ?? 0;
1172
+ const startLine = Math.max(1, current.body.slice(0, at).split("\n").length - 2);
1173
+ const part = snippet(current.body, { startLine, maxChars: 1600 });
1174
+ hits.push({
1175
+ path: item.path,
1176
+ title: item.title,
1177
+ hash: current.hash,
1178
+ kind: item.kind,
1179
+ rank: item.rank,
1180
+ ...part
1181
+ });
1182
+ }
1183
+ return {
1184
+ query: args.query,
1185
+ hits,
1186
+ ...status(),
1187
+ pendingMaintenance: pending(args.project, 8),
1188
+ staleCandidates,
1189
+ complete: staleCandidates === 0 && coverage === "ready" && dirty.size === 0 && db.prepare("SELECT count(*) count FROM problems").get().count === 0,
1190
+ warning: coverage === "ready" ? null : "Index is still being reconciled; zero hits are not proof of absence."
1191
+ };
1192
+ }
1193
+ async function hydrateCandidates(args) {
1194
+ const candidates = Array.isArray(args.candidates) ? args.candidates.slice(0, 24) : Array.isArray(args.paths) ? [...new Set(args.paths)].slice(0, 24).map((path) => ({ path })) : [];
1195
+ const limit = Math.max(1, Math.min(12, Math.floor(args.limit || 5)));
1196
+ const hits = [];
1197
+ for (const candidate of candidates) {
1198
+ if (hits.length >= limit || typeof candidate?.path !== "string") break;
1199
+ let path;
1200
+ try {
1201
+ path = validateNote(candidate.path);
1202
+ if (!canRead(path, args.project)) continue;
1203
+ } catch {
1204
+ continue;
1205
+ }
1206
+ let current;
1207
+ try {
1208
+ current = await updatePath(path);
1209
+ } catch {
1210
+ continue;
1211
+ }
1212
+ if (!current || current.kind === "explainer" || candidate.hash && candidate.hash !== current.hash)
1213
+ continue;
1214
+ const startChar = Number.isInteger(candidate.startChar) ? candidate.startChar : -1;
1215
+ const startLine = startChar >= 0 && startChar <= current.body.length ? current.body.slice(0, startChar).split("\n").length : 1;
1216
+ const part = snippet(current.body, { startLine, maxChars: 1600 });
1217
+ hits.push({
1218
+ path,
1219
+ title: current.title,
1220
+ hash: current.hash,
1221
+ kind: current.kind,
1222
+ rank: null,
1223
+ ...Number.isInteger(candidate.chunkIndex) ? { chunkIndex: candidate.chunkIndex } : {},
1224
+ ...part
1225
+ });
1226
+ }
1227
+ return { hits };
1228
+ }
1229
+ async function semanticBatch(args) {
1230
+ const fingerprint = String(args.fingerprint || "");
1231
+ if (!fingerprint || fingerprint.length > 300) throw new Error("Invalid semantic fingerprint");
1232
+ const limit = Math.max(1, Math.min(16, Math.floor(args.limit || 8)));
1233
+ const chunkChars = Math.max(256, Math.min(8e3, Math.floor(args.chunkChars || 1200)));
1234
+ const scope = args.project || "";
1235
+ const rows = db.prepare(`SELECT path,title,body,hash,signature,scope,kind FROM notes
1236
+ WHERE (scope='shared' OR scope=?) AND kind NOT IN ('navigation','explainer') ORDER BY path`).all(scope);
1237
+ const items = [];
1238
+ for (const row of rows) {
1239
+ let current;
1240
+ try {
1241
+ if (!canRead(row.path, args.project)) continue;
1242
+ if (/(?:^|\/)(?:Runs|Explainers)\//i.test(row.path)) continue;
1243
+ current = await updatePath(row.path);
1244
+ if (!current || current.hash !== row.hash || current.signature !== row.signature) continue;
1245
+ } catch {
1246
+ continue;
1247
+ }
1248
+ const chunks = splitKnowledgeChunks(current.body, chunkChars);
1249
+ for (let index = 0; index < chunks.length; index++) {
1250
+ const chunk = chunks[index];
1251
+ const found = db.prepare(
1252
+ "SELECT 1 FROM semantic_chunks WHERE path=? AND chunk_index=? AND hash=? AND fingerprint=? LIMIT 1"
1253
+ ).get(row.path, index, row.hash, fingerprint);
1254
+ if (found) continue;
1255
+ if (items.length >= limit) {
1256
+ return { items, partial: true, nextCursor: null };
1257
+ }
1258
+ const startChar = index * chunkChars;
1259
+ items.push({
1260
+ path: row.path,
1261
+ chunkIndex: index,
1262
+ hash: current.hash,
1263
+ mtime: current.signature,
1264
+ text: chunk,
1265
+ startChar,
1266
+ endChar: startChar + chunk.length
1267
+ });
1268
+ }
1269
+ }
1270
+ return { items, partial: false, nextCursor: null };
1271
+ }
1272
+ function semanticStore(args) {
1273
+ const fingerprint = String(args.fingerprint || "");
1274
+ if (!fingerprint || fingerprint.length > 300 || !Array.isArray(args.items) || args.items.length > 32)
1275
+ throw new Error("Invalid semantic storage request");
1276
+ let dimension = null;
1277
+ db.exec("BEGIN IMMEDIATE");
1278
+ try {
1279
+ for (const item of args.items) {
1280
+ const row = statements.get.get(item.path);
1281
+ if (!row || row.hash !== item.hash || row.signature !== item.mtime || !Array.isArray(item.vector) || !Number.isInteger(item.chunkIndex) || item.chunkIndex < 0)
1282
+ throw new Error("Semantic batch is stale");
1283
+ const vector = item.vector;
1284
+ if (!vector.length || vector.length > 16384 || vector.some((n) => typeof n !== "number" || !Number.isFinite(n) || Math.abs(n) > 1e6))
1285
+ throw new Error("Invalid semantic vector");
1286
+ let norm = 0;
1287
+ for (const value of vector) {
1288
+ norm += value * value;
1289
+ if (!Number.isFinite(norm) || norm > 1e18) throw new Error("Invalid semantic vector norm");
1290
+ }
1291
+ if (!(norm > 0)) throw new Error("Invalid semantic vector norm");
1292
+ if (dimension === null) dimension = vector.length;
1293
+ if (vector.length !== dimension) throw new Error("Semantic batch dimensions do not match");
1294
+ const meta = db.prepare("SELECT dimension FROM semantic_meta WHERE fingerprint=?").get(fingerprint);
1295
+ if (meta && Number(meta.dimension) !== vector.length)
1296
+ throw new Error("Semantic fingerprint dimension mismatch");
1297
+ db.prepare(
1298
+ "INSERT OR REPLACE INTO semantic_chunks(path,chunk_index,hash,fingerprint,mtime,vector,dimension,start_char,end_char) VALUES(?,?,?,?,?,?,?,?,?)"
1299
+ ).run(
1300
+ item.path,
1301
+ item.chunkIndex,
1302
+ item.hash,
1303
+ fingerprint,
1304
+ item.mtime,
1305
+ JSON.stringify(vector),
1306
+ vector.length,
1307
+ Number.isInteger(item.startChar) ? item.startChar : null,
1308
+ Number.isInteger(item.endChar) ? item.endChar : null
1309
+ );
1310
+ }
1311
+ if (dimension !== null)
1312
+ db.prepare(
1313
+ "INSERT INTO semantic_meta(fingerprint,dimension) VALUES(?,?) ON CONFLICT(fingerprint) DO UPDATE SET dimension=excluded.dimension"
1314
+ ).run(fingerprint, dimension);
1315
+ db.exec("COMMIT");
1316
+ } catch (error) {
1317
+ db.exec("ROLLBACK");
1318
+ throw error;
1319
+ }
1320
+ return status();
1321
+ }
1322
+ function semanticCandidates(args) {
1323
+ if (!Array.isArray(args.vector) || !args.vector.length || args.vector.some((n) => typeof n !== "number" || !Number.isFinite(n) || Math.abs(n) > 1e6))
1324
+ throw new Error("Invalid semantic query vector");
1325
+ const fingerprint = String(args.fingerprint || "");
1326
+ if (!fingerprint || fingerprint.length > 300) throw new Error("Invalid semantic fingerprint");
1327
+ const minSimilarity = args.minSimilarity === void 0 ? 0.45 : args.minSimilarity;
1328
+ if (typeof minSimilarity !== "number" || !Number.isFinite(minSimilarity) || minSimilarity < 0 || minSimilarity > 1)
1329
+ throw new Error("Invalid semantic similarity threshold");
1330
+ const query = args.vector, limit = Math.max(1, Math.min(12, Math.floor(args.limit || 5)));
1331
+ const rows = db.prepare(
1332
+ "SELECT s.path,s.hash,s.vector,s.dimension,s.chunk_index,s.start_char,s.end_char,n.title,n.kind FROM semantic_chunks s JOIN notes n ON n.path=s.path AND n.hash=s.hash WHERE s.fingerprint=? AND (n.scope='shared' OR n.scope=?) AND n.kind!='explainer' ORDER BY s.path LIMIT 5000"
1333
+ ).all(fingerprint, args.project || "");
1334
+ const totalEligible = Number(
1335
+ db.prepare(
1336
+ "SELECT COUNT(*) AS count FROM semantic_chunks s JOIN notes n ON n.path=s.path AND n.hash=s.hash WHERE s.fingerprint=? AND (n.scope='shared' OR n.scope=?) AND n.kind!='explainer'"
1337
+ ).get(fingerprint, args.project || "").count
1338
+ );
1339
+ const best = /* @__PURE__ */ new Map();
1340
+ for (const row of rows) {
1341
+ if (row.dimension !== query.length) continue;
1342
+ let vector;
1343
+ try {
1344
+ vector = JSON.parse(row.vector);
1345
+ } catch {
1346
+ continue;
1347
+ }
1348
+ if (!Array.isArray(vector) || vector.length !== query.length) continue;
1349
+ let dot = 0, qa = 0, va = 0;
1350
+ for (let i = 0; i < query.length; i++) {
1351
+ const q = query[i], v = vector[i];
1352
+ if (typeof v !== "number" || !Number.isFinite(v)) {
1353
+ va = Number.NaN;
1354
+ break;
1355
+ }
1356
+ dot += q * v;
1357
+ qa += q * q;
1358
+ va += v * v;
1359
+ }
1360
+ const score = dot / Math.sqrt(qa * va);
1361
+ if (!Number.isFinite(score) || score < minSimilarity) continue;
1362
+ const current = best.get(row.path);
1363
+ if (!current || score > current.score)
1364
+ best.set(row.path, {
1365
+ path: row.path,
1366
+ title: row.title,
1367
+ hash: row.hash,
1368
+ kind: row.kind,
1369
+ score,
1370
+ chunkIndex: row.chunk_index,
1371
+ startChar: row.start_char,
1372
+ endChar: row.end_char
1373
+ });
1374
+ }
1375
+ const result = [...best.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, limit);
1376
+ return {
1377
+ items: result,
1378
+ partial: totalEligible > rows.length,
1379
+ scanned: rows.length,
1380
+ totalEligible,
1381
+ nextCursor: null
1382
+ };
1383
+ }
1384
+ function resolveLinkTarget(target) {
1385
+ if (statements.get.get(target)) return target;
1386
+ if (target.includes("/")) return null;
1387
+ const rows = db.prepare("SELECT path FROM notes WHERE path LIKE ? LIMIT 2").all(`%/${target}`);
1388
+ return rows.length === 1 ? rows[0].path : null;
1389
+ }
1390
+ function noteTitle(path) {
1391
+ return db.prepare("SELECT title FROM notes WHERE path=?").get(path)?.title || path.split("/").at(-1).slice(0, -3);
1392
+ }
1393
+ async function noteLinks(args) {
1394
+ await flushDirty();
1395
+ const path = validateNote(String(args.path || ""));
1396
+ const outgoing = [];
1397
+ for (const { target } of db.prepare("SELECT target FROM links WHERE source=? ORDER BY target").all(path)) {
1398
+ const resolved = resolveLinkTarget(target);
1399
+ outgoing.push(
1400
+ resolved ? { path: resolved, title: noteTitle(resolved), exists: true } : { path: target, title: target.split("/").at(-1).slice(0, -3), exists: false }
1401
+ );
1402
+ }
1403
+ const name = path.split("/").at(-1);
1404
+ const targets = resolveLinkTarget(name) === path ? [path, name] : [path, path];
1405
+ const incoming = db.prepare("SELECT DISTINCT source FROM links WHERE target=? OR target=? ORDER BY source LIMIT 200").all(...targets).map(({ source }) => source).filter((source) => source !== path).map((source) => ({ path: source, title: noteTitle(source), exists: true }));
1406
+ return { path, title: noteTitle(path), outgoing, incoming, revision };
1407
+ }
1408
+ async function graphNeighbors(args) {
1409
+ await flushDirty();
1410
+ const project = args.project || "";
1411
+ const seeds = (Array.isArray(args.paths) ? args.paths : []).slice(0, 6).map(String);
1412
+ const per = Math.max(1, Math.min(12, Math.floor(args.perSeed || 8)));
1413
+ const noteRow = db.prepare("SELECT path,title,kind,hash,body,scope FROM notes WHERE path=?");
1414
+ const visible = (row) => row && (row.kind === "note" || row.kind === "wiki") && (row.scope === "shared" || row.scope === project);
1415
+ const degreeOf = (path) => {
1416
+ const name = path.split("/").at(-1);
1417
+ const out = db.prepare("SELECT COUNT(*) c FROM links WHERE source=?").get(path).c;
1418
+ const inc = db.prepare("SELECT COUNT(DISTINCT source) c FROM links WHERE target=? OR target=?").get(path, name).c;
1419
+ return out + inc;
1420
+ };
1421
+ const items = [];
1422
+ for (const seed of seeds) {
1423
+ let seedPath;
1424
+ try {
1425
+ seedPath = validateNote(seed);
1426
+ } catch {
1427
+ continue;
1428
+ }
1429
+ const seedRow = noteRow.get(seedPath);
1430
+ if (!visible(seedRow)) continue;
1431
+ const found = /* @__PURE__ */ new Map();
1432
+ for (const { target } of db.prepare("SELECT target FROM links WHERE source=? ORDER BY target LIMIT 64").all(seedPath)) {
1433
+ const resolved = resolveLinkTarget(target);
1434
+ if (resolved && resolved !== seedPath && !found.has(resolved)) found.set(resolved, { direction: "outgoing" });
1435
+ }
1436
+ const name = seedPath.split("/").at(-1);
1437
+ const targets = resolveLinkTarget(name) === seedPath ? [seedPath, name] : [seedPath, seedPath];
1438
+ for (const { source } of db.prepare("SELECT DISTINCT source FROM links WHERE target=? OR target=? ORDER BY source LIMIT 64").all(...targets))
1439
+ if (source !== seedPath && !found.has(source)) found.set(source, { direction: "backlink" });
1440
+ const rows = [];
1441
+ for (const [path, meta] of found) {
1442
+ const row = noteRow.get(path);
1443
+ if (!visible(row)) continue;
1444
+ rows.push({
1445
+ from: seedPath,
1446
+ path,
1447
+ title: row.title,
1448
+ kind: row.kind,
1449
+ hash: row.hash,
1450
+ direction: meta.direction,
1451
+ degree: degreeOf(path),
1452
+ ...meta.direction === "backlink" ? { context: backlinkContext(row.body, seedPath) } : {}
1453
+ });
1454
+ }
1455
+ rows.sort((a, b) => b.degree - a.degree || a.path.localeCompare(b.path));
1456
+ items.push(...rows.slice(0, per));
1457
+ }
1458
+ return { items, revision };
1459
+ }
1460
+ function noteIdentity(body) {
1461
+ if (typeof body !== "string") return null;
1462
+ const match = body.match(/^(?:---[\s\S]*?\n)?(?:doi|DOI|pmid|PMID|arxiv|ArXiv)\s*:\s*([^\s#]+)\s*$/m);
1463
+ if (!match?.[1]) return null;
1464
+ const field = match[0].split(":", 1)[0]?.toLowerCase();
1465
+ return `${field}:${match[1].replace(/[<>"']/g, "").trim()}`;
1466
+ }
1467
+ async function knowledgeGraph(args) {
1468
+ await flushDirty();
1469
+ revision = Number(db.prepare("SELECT value FROM meta WHERE key='revision'").get().value);
1470
+ const limit = Math.max(10, Math.min(400, Math.floor(args.limit || 200)));
1471
+ const view = args.view === "all" ? "all" : "semantic";
1472
+ const mergeMirrors = args.mergeMirrors !== false;
1473
+ const cacheKey = `${view}\0${limit}\0${mergeMirrors ? "merge" : "raw"}`;
1474
+ const cached = graphSnapshots.get(revision, cacheKey);
1475
+ if (cached) return cached;
1476
+ const allNotes = new Map(
1477
+ db.prepare("SELECT path,title,kind,scope,body FROM notes").all().map((row) => [row.path, row])
1478
+ );
1479
+ const notes = new Map([...allNotes].filter(([, row]) => row.kind === "note" || row.kind === "wiki"));
1480
+ const rawEdges = [];
1481
+ for (const { source, target } of db.prepare("SELECT source,target FROM links").all()) {
1482
+ if (!notes.has(source)) continue;
1483
+ const resolved = resolveLinkTarget(target);
1484
+ if (!resolved || resolved === source || !notes.has(resolved)) continue;
1485
+ rawEdges.push({ source, target: resolved, type: "link" });
1486
+ }
1487
+ const degree = /* @__PURE__ */ new Map();
1488
+ for (const edge of rawEdges) {
1489
+ degree.set(edge.source, (degree.get(edge.source) || 0) + 1);
1490
+ degree.set(edge.target, (degree.get(edge.target) || 0) + 1);
1491
+ }
1492
+ const legacyEdges = /* @__PURE__ */ new Map();
1493
+ for (const edge of rawEdges) {
1494
+ const key = edge.source < edge.target ? `${edge.source}\0${edge.target}` : `${edge.target}\0${edge.source}`;
1495
+ legacyEdges.set(key, [edge.source, edge.target]);
1496
+ }
1497
+ const legacyDegree = /* @__PURE__ */ new Map();
1498
+ for (const [source, target] of legacyEdges.values()) {
1499
+ legacyDegree.set(source, (legacyDegree.get(source) || 0) + 1);
1500
+ legacyDegree.set(target, (legacyDegree.get(target) || 0) + 1);
1501
+ }
1502
+ const legacyChosen = [...notes.keys()].sort((a, b) => (legacyDegree.get(b) || 0) - (legacyDegree.get(a) || 0) || a.localeCompare(b)).slice(0, limit);
1503
+ const legacyKeep = new Set(legacyChosen);
1504
+ const semanticCandidateLimit = Math.min(1600, Math.max(limit, limit * 4));
1505
+ const neighbours = new Map(legacyChosen.map((path) => [path, []]));
1506
+ for (const edge of rawEdges) {
1507
+ if (neighbours.has(edge.source)) neighbours.get(edge.source).push(edge.target);
1508
+ if (neighbours.has(edge.target)) neighbours.get(edge.target).push(edge.source);
1509
+ }
1510
+ for (const paths of neighbours.values())
1511
+ paths.sort((a, b) => (degree.get(b) || 0) - (degree.get(a) || 0) || a.localeCompare(b));
1512
+ const semanticCandidatePaths = new Set(legacyChosen);
1513
+ for (let offset = 0; semanticCandidatePaths.size < semanticCandidateLimit; offset++) {
1514
+ let added = false;
1515
+ for (const path of legacyChosen) {
1516
+ const neighbour = neighbours.get(path)?.[offset];
1517
+ if (!neighbour || semanticCandidatePaths.size >= semanticCandidateLimit) continue;
1518
+ const previousSize = semanticCandidatePaths.size;
1519
+ semanticCandidatePaths.add(neighbour);
1520
+ added ||= semanticCandidatePaths.size !== previousSize;
1521
+ }
1522
+ if (!added) break;
1523
+ }
1524
+ const semanticCandidateNodes = [...semanticCandidatePaths].map((path) => allNotes.get(path)).filter(Boolean);
1525
+ const semanticCandidateEdges = rawEdges.filter(
1526
+ (edge) => semanticCandidatePaths.has(edge.source) && semanticCandidatePaths.has(edge.target)
1527
+ );
1528
+ const duplicateCounts = /* @__PURE__ */ new Map();
1529
+ if (mergeMirrors) {
1530
+ for (const row of notes.values()) {
1531
+ const identity = noteIdentity(row.body);
1532
+ if (identity) duplicateCounts.set(identity, (duplicateCounts.get(identity) || 0) + 1);
1533
+ }
1534
+ }
1535
+ const totalDuplicateCount = [...duplicateCounts.values()].reduce((sum, count) => sum + Math.max(0, count - 1), 0);
1536
+ const model = createSemanticModel(
1537
+ {
1538
+ nodes: semanticCandidateNodes.map((row) => ({
1539
+ path: row.path,
1540
+ title: row.title,
1541
+ kind: row.kind,
1542
+ scope: row.scope,
1543
+ degree: degree.get(row.path) || 0,
1544
+ identity: noteIdentity(row.body),
1545
+ contentType: (() => {
1546
+ const type = parseFrontmatter(row.body).data?.type;
1547
+ return typeof type === "string" ? type : null;
1548
+ })()
1549
+ })),
1550
+ edges: semanticCandidateEdges
1551
+ },
1552
+ { view, mergeMirrors }
1553
+ );
1554
+ const chosen = [...model.nodes].sort((a, b) => b.degree - a.degree || a.path.localeCompare(b.path)).slice(0, limit);
1555
+ const keep = new Set(chosen.map((node) => node.path));
1556
+ const links = model.edges.filter(({ source, target }) => keep.has(source) && keep.has(target));
1557
+ const relations = (model.relations || []).filter(({ source, target }) => keep.has(source) && keep.has(target));
1558
+ const result = {
1559
+ nodes: legacyChosen.map((path) => ({ path, title: notes.get(path).title, kind: notes.get(path).kind, degree: legacyDegree.get(path) || 0 })),
1560
+ edges: [...legacyEdges.values()].filter(([source, target]) => legacyKeep.has(source) && legacyKeep.has(target)).map(([source, target]) => ({ source, target })),
1561
+ totalNotes: notes.size,
1562
+ revision,
1563
+ semantic: {
1564
+ nodes: chosen.map((node) => ({ ...node })),
1565
+ edges: links,
1566
+ totalNotes: notes.size,
1567
+ view,
1568
+ mergeMirrors,
1569
+ relations,
1570
+ duplicates: (model.duplicates || []).filter(({ canonical, duplicate }) => keep.has(canonical) || keep.has(duplicate)),
1571
+ communities: Object.fromEntries(Object.entries(model.communities || {}).filter(([path]) => keep.has(path))),
1572
+ layout: Object.fromEntries(Object.entries(model.layout || {}).filter(([path]) => keep.has(path))),
1573
+ health: {
1574
+ isolated: chosen.filter((node) => node.degree === 0).length,
1575
+ duplicates: totalDuplicateCount,
1576
+ infrastructure: chosen.filter((node) => node.isInfrastructure).length,
1577
+ truncated: semanticCandidatePaths.size < notes.size,
1578
+ candidateNodes: semanticCandidatePaths.size,
1579
+ candidateLimit: semanticCandidateLimit
1580
+ }
1581
+ }
1582
+ };
1583
+ graphSnapshots.set(revision, cacheKey, result);
1584
+ return result;
1585
+ }
1586
+ async function dispatch(op, args) {
1587
+ if (op === "status") {
1588
+ if (args.flushPending) await flushDirty();
1589
+ return { ...status(), semanticScoped: semanticStatus(args.fingerprint, args.project) };
1590
+ }
1591
+ if (op === "read") return read(args.path, args.project, args);
1592
+ if (op === "search") return search(args);
1593
+ if (op === "noteLinks") return noteLinks(args);
1594
+ if (op === "graph") return knowledgeGraph(args);
1595
+ if (op === "graphNeighbors") return graphNeighbors(args);
1596
+ if (op === "hydrateCandidates") return hydrateCandidates(args);
1597
+ if (op === "semanticBatch") return semanticBatch(args);
1598
+ if (op === "semanticStore") return semanticStore(args);
1599
+ if (op === "semanticCandidates") return semanticCandidates(args);
1600
+ if (op === "warm") {
1601
+ void beginReconcile();
1602
+ return status();
1603
+ }
1604
+ if (op === "reconcile") {
1605
+ const pendingScan = Boolean(scan);
1606
+ fullScanRequested = false;
1607
+ await beginReconcile(!!args.force);
1608
+ if (pendingScan) await beginReconcile(!!args.force);
1609
+ await flushDirty();
1610
+ if (scan) await scan;
1611
+ return status();
1612
+ }
1613
+ if (op === "changed") {
1614
+ for (const path of args.paths.slice(0, 100)) {
1615
+ const previous = statements.get.get(path);
1616
+ let signature = null;
1617
+ try {
1618
+ signature = (await inspectNote(workerData.vault, path)).signature;
1619
+ } catch {
1620
+ }
1621
+ if (previous?.signature === signature && signature) continue;
1622
+ await updatePath(path, true);
1623
+ dirty.delete(path);
1624
+ }
1625
+ return status();
1626
+ }
1627
+ if (op === "enqueueNavigation") {
1628
+ const project = args.project;
1629
+ if (project && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(project)) throw new Error("Invalid project scope");
1630
+ if (project) job("navigation", `Projects/${project}/Index.md`, project);
1631
+ job("navigation", "Library/Index.md", "shared");
1632
+ return status();
1633
+ }
1634
+ if (op === "uiJobs") {
1635
+ const offset = Math.max(0, Math.min(1e5, Math.floor(args.offset || 0))), limit = Math.max(1, Math.min(50, Math.floor(args.limit || 20)));
1636
+ const scope = args.project || "";
1637
+ const where = "(scope='shared' OR scope=?)";
1638
+ const total = Number(db.prepare(`SELECT count(*) n FROM jobs WHERE ${where}`).get(scope).n);
1639
+ const items = db.prepare(`SELECT * FROM jobs WHERE ${where} ORDER BY updated DESC,key LIMIT ? OFFSET ?`).all(scope, limit, offset);
1640
+ return { items, total, offset, nextOffset: offset + items.length < total ? offset + items.length : null };
1641
+ }
1642
+ if (op === "jobs") return { ...status(), items: pending(args.project, 100, args.kind || null) };
1643
+ if (op === "completeJob") {
1644
+ db.prepare("DELETE FROM jobs WHERE key=? AND revision=?").run(args.key, args.revision);
1645
+ return status();
1646
+ }
1647
+ if (op === "navigation") {
1648
+ const scope = args.project || "shared";
1649
+ const filter = args.targetPath === "Wiki/Index.md" ? "AND kind='wiki' AND path LIKE 'Wiki/%'" : args.targetPath === "Library/Index.md" ? "AND path LIKE 'Library/%'" : "AND kind!='navigation'";
1650
+ return db.prepare(`SELECT path,title,kind FROM notes WHERE scope=? ${filter} ORDER BY path LIMIT 61`).all(scope);
1651
+ }
1652
+ throw new Error("Unknown knowledge operation");
1653
+ }
1654
+ parentPort.on("message", async ({ id, op, args = {} }) => {
1655
+ if (op === "close") {
1656
+ try {
1657
+ await closeWorker();
1658
+ parentPort.postMessage({ id, result: { closed: true } });
1659
+ parentPort.close();
1660
+ } catch (error) {
1661
+ parentPort.postMessage({ id, error: String(error.message) });
1662
+ }
1663
+ return;
1664
+ }
1665
+ if (_closed) {
1666
+ parentPort.postMessage({ id, error: "Knowledge worker is closing" });
1667
+ return;
1668
+ }
1669
+ const task = dispatch(op, args);
1670
+ activeRequests.add(task);
1671
+ try {
1672
+ const result = await task;
1673
+ parentPort.postMessage({ id, result });
1674
+ } catch (error) {
1675
+ parentPort.postMessage({ id, error: String(error.message) });
1676
+ } finally {
1677
+ activeRequests.delete(task);
1678
+ }
1679
+ });
1680
+ parentPort.postMessage({ ready: true });