@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,29 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as yieldToEventLoop, f as sha256File, l as chunkMarkdown, n as chunkId } from "./helpers-BEST-4Gx.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, normalize, relative } from "node:path";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
4
|
|
|
6
|
-
//#region src/memory/vault
|
|
5
|
+
//#region src/memory/vault/walk.ts
|
|
7
6
|
/**
|
|
8
|
-
* Vault
|
|
9
|
-
*
|
|
10
|
-
* Indexes an entire Obsidian vault (or any markdown knowledge base), following
|
|
11
|
-
* symlinks, deduplicating files by inode, parsing wikilinks, and computing
|
|
12
|
-
* per-file health metrics (orphan detection, dead links).
|
|
13
|
-
*
|
|
14
|
-
* Key differences from the project indexer (indexer.ts):
|
|
15
|
-
* - Follows symbolic links (project indexer skips them)
|
|
16
|
-
* - Deduplicates files with the same inode (same content reachable via multiple paths)
|
|
17
|
-
* - Parses [[wikilinks]] and builds a directed link graph
|
|
18
|
-
* - Resolves wikilinks using Obsidian's shortest-match algorithm
|
|
19
|
-
* - Computes health metrics per file: inbound/outbound link counts, dead links, orphans
|
|
7
|
+
* Vault directory walker — follows symlinks with cycle detection.
|
|
20
8
|
*/
|
|
21
9
|
/** Maximum number of .md files to collect from a vault. */
|
|
22
10
|
const VAULT_MAX_FILES = 1e4;
|
|
23
11
|
/** Maximum recursion depth for vault directory walks. */
|
|
24
12
|
const VAULT_MAX_DEPTH = 10;
|
|
25
|
-
/** Number of files to process before yielding to the event loop. */
|
|
26
|
-
const VAULT_YIELD_EVERY = 10;
|
|
27
13
|
/**
|
|
28
14
|
* Directories to always skip, at any depth, during vault walks.
|
|
29
15
|
* Includes standard build/VCS noise plus Obsidian-specific directories.
|
|
@@ -46,15 +32,6 @@ const VAULT_SKIP_DIRS = new Set([
|
|
|
46
32
|
".obsidian",
|
|
47
33
|
".trash"
|
|
48
34
|
]);
|
|
49
|
-
function sha256File(content) {
|
|
50
|
-
return createHash("sha256").update(content).digest("hex");
|
|
51
|
-
}
|
|
52
|
-
function chunkId(projectId, path, chunkIndex, startLine, endLine) {
|
|
53
|
-
return createHash("sha256").update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`).digest("hex");
|
|
54
|
-
}
|
|
55
|
-
function yieldToEventLoop() {
|
|
56
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
57
|
-
}
|
|
58
35
|
/**
|
|
59
36
|
* Recursively collect all .md files under a vault root, following symlinks.
|
|
60
37
|
*
|
|
@@ -66,11 +43,8 @@ function yieldToEventLoop() {
|
|
|
66
43
|
* Using the real stat (not lstat) ensures that symlinked dirs resolve to
|
|
67
44
|
* their actual inode, preventing infinite loops.
|
|
68
45
|
*
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
* @param acc Shared accumulator (mutated in place for early exit).
|
|
72
|
-
* @param visited Set of "device:inode" strings for visited directories.
|
|
73
|
-
* @param depth Current recursion depth.
|
|
46
|
+
* @param vaultRoot Absolute path to the vault root directory.
|
|
47
|
+
* @param opts Optional overrides for maxFiles and maxDepth.
|
|
74
48
|
*/
|
|
75
49
|
function walkVaultMdFiles(vaultRoot, opts) {
|
|
76
50
|
const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;
|
|
@@ -137,6 +111,9 @@ function walkVaultMdFiles(vaultRoot, opts) {
|
|
|
137
111
|
if (existsSync(vaultRoot)) walk(vaultRoot, 0);
|
|
138
112
|
return results;
|
|
139
113
|
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/memory/vault/deduplicate.ts
|
|
140
117
|
/**
|
|
141
118
|
* Group vault files by inode identity (device + inode).
|
|
142
119
|
*
|
|
@@ -168,6 +145,9 @@ function deduplicateByInode(files) {
|
|
|
168
145
|
}
|
|
169
146
|
return result;
|
|
170
147
|
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/memory/vault/parse-links.ts
|
|
171
151
|
/**
|
|
172
152
|
* Parse all links from markdown content.
|
|
173
153
|
*
|
|
@@ -246,6 +226,12 @@ function parseLinks(content) {
|
|
|
246
226
|
}
|
|
247
227
|
return links;
|
|
248
228
|
}
|
|
229
|
+
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/memory/vault/name-index.ts
|
|
232
|
+
/**
|
|
233
|
+
* Name index builder for Obsidian wikilink resolution.
|
|
234
|
+
*/
|
|
249
235
|
/**
|
|
250
236
|
* Build a name lookup index for Obsidian wikilink resolution.
|
|
251
237
|
*
|
|
@@ -263,6 +249,12 @@ function buildNameIndex(files) {
|
|
|
263
249
|
}
|
|
264
250
|
return index;
|
|
265
251
|
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/memory/vault/resolve.ts
|
|
255
|
+
/**
|
|
256
|
+
* Wikilink resolver using Obsidian's shortest-match algorithm.
|
|
257
|
+
*/
|
|
266
258
|
/**
|
|
267
259
|
* Resolve a wikilink target to a vault-relative path using Obsidian's rules.
|
|
268
260
|
*
|
|
@@ -326,9 +318,28 @@ function commonPrefixLength(a, b) {
|
|
|
326
318
|
else break;
|
|
327
319
|
return count;
|
|
328
320
|
}
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/memory/vault/indexer.ts
|
|
324
|
+
/**
|
|
325
|
+
* Main vault indexing orchestrator.
|
|
326
|
+
*
|
|
327
|
+
* Indexes an entire Obsidian vault (or any markdown knowledge base), following
|
|
328
|
+
* symlinks, deduplicating files by inode, parsing wikilinks, and computing
|
|
329
|
+
* per-file health metrics (orphan detection, dead links).
|
|
330
|
+
*
|
|
331
|
+
* Key differences from the project indexer (indexer/sync.ts):
|
|
332
|
+
* - Follows symbolic links (project indexer skips them)
|
|
333
|
+
* - Deduplicates files with the same inode (same content reachable via multiple paths)
|
|
334
|
+
* - Parses [[wikilinks]] and builds a directed link graph
|
|
335
|
+
* - Resolves wikilinks using Obsidian's shortest-match algorithm
|
|
336
|
+
* - Computes health metrics per file: inbound/outbound link counts, dead links, orphans
|
|
337
|
+
*/
|
|
338
|
+
/** Number of files to process before yielding to the event loop. */
|
|
339
|
+
const VAULT_YIELD_EVERY = 1;
|
|
329
340
|
/**
|
|
330
|
-
* Index an entire Obsidian vault (or markdown knowledge base)
|
|
331
|
-
*
|
|
341
|
+
* Index an entire Obsidian vault (or markdown knowledge base) using the
|
|
342
|
+
* async StorageBackend interface.
|
|
332
343
|
*
|
|
333
344
|
* Steps:
|
|
334
345
|
* 1. Walk vault root, following symlinks.
|
|
@@ -337,7 +348,7 @@ function commonPrefixLength(a, b) {
|
|
|
337
348
|
* 4. For each canonical file:
|
|
338
349
|
* a. SHA-256 hash for change detection — skip unchanged files.
|
|
339
350
|
* b. Read content, chunk with chunkMarkdown().
|
|
340
|
-
* c. Insert chunks into memory_chunks and memory_fts.
|
|
351
|
+
* c. Insert chunks into backend (memory_chunks and memory_fts).
|
|
341
352
|
* d. Upsert vault_files row.
|
|
342
353
|
* 5. Record aliases in vault_aliases.
|
|
343
354
|
* 6. Rebuild vault_name_index table.
|
|
@@ -348,11 +359,11 @@ function commonPrefixLength(a, b) {
|
|
|
348
359
|
* 8. Compute and upsert health metrics (vault_health).
|
|
349
360
|
* 9. Return statistics.
|
|
350
361
|
*
|
|
351
|
-
* @param
|
|
362
|
+
* @param backend StorageBackend to write to.
|
|
352
363
|
* @param vaultProjectId Registry project ID for the vault "project".
|
|
353
364
|
* @param vaultRoot Absolute path to the vault root directory.
|
|
354
365
|
*/
|
|
355
|
-
async function indexVault(
|
|
366
|
+
async function indexVault(backend, vaultProjectId, vaultRoot) {
|
|
356
367
|
const startTime = Date.now();
|
|
357
368
|
const result = {
|
|
358
369
|
filesIndexed: 0,
|
|
@@ -367,28 +378,6 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
367
378
|
const allFiles = walkVaultMdFiles(vaultRoot);
|
|
368
379
|
const inodeGroups = deduplicateByInode(allFiles);
|
|
369
380
|
const nameIndex = buildNameIndex(allFiles);
|
|
370
|
-
const selectFileHash = db.prepare("SELECT hash FROM vault_files WHERE vault_path = ?");
|
|
371
|
-
const deleteOldChunkIds = db.prepare("SELECT id FROM memory_chunks WHERE project_id = ? AND path = ?");
|
|
372
|
-
const deleteFts = db.prepare("DELETE FROM memory_fts WHERE id = ?");
|
|
373
|
-
const deleteChunks = db.prepare("DELETE FROM memory_chunks WHERE project_id = ? AND path = ?");
|
|
374
|
-
const insertChunk = db.prepare(`
|
|
375
|
-
INSERT INTO memory_chunks (id, project_id, source, tier, path, start_line, end_line, hash, text, updated_at)
|
|
376
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
377
|
-
`);
|
|
378
|
-
const insertFts = db.prepare(`
|
|
379
|
-
INSERT INTO memory_fts (text, id, project_id, path, source, tier, start_line, end_line)
|
|
380
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
381
|
-
`);
|
|
382
|
-
const upsertVaultFile = db.prepare(`
|
|
383
|
-
INSERT INTO vault_files (vault_path, inode, device, hash, title, indexed_at)
|
|
384
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
385
|
-
ON CONFLICT(vault_path) DO UPDATE SET
|
|
386
|
-
inode = excluded.inode,
|
|
387
|
-
device = excluded.device,
|
|
388
|
-
hash = excluded.hash,
|
|
389
|
-
title = excluded.title,
|
|
390
|
-
indexed_at = excluded.indexed_at
|
|
391
|
-
`);
|
|
392
381
|
await yieldToEventLoop();
|
|
393
382
|
let filesSinceYield = 0;
|
|
394
383
|
for (const group of inodeGroups) {
|
|
@@ -406,64 +395,74 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
406
395
|
continue;
|
|
407
396
|
}
|
|
408
397
|
const hash = sha256File(content);
|
|
409
|
-
if (
|
|
398
|
+
if ((await backend.getVaultFile(canonical.vaultRelPath))?.hash === hash) {
|
|
410
399
|
result.filesSkipped++;
|
|
411
400
|
continue;
|
|
412
401
|
}
|
|
413
|
-
|
|
414
|
-
db.transaction(() => {
|
|
415
|
-
for (const row of oldChunkIds) deleteFts.run(row.id);
|
|
416
|
-
deleteChunks.run(vaultProjectId, canonical.vaultRelPath);
|
|
417
|
-
})();
|
|
402
|
+
await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);
|
|
418
403
|
const chunks = chunkMarkdown(content);
|
|
419
404
|
const updatedAt = Date.now();
|
|
420
405
|
const titleMatch = /^#\s+(.+)$/m.exec(content);
|
|
421
406
|
const title = titleMatch ? titleMatch[1].trim() : basename(canonical.vaultRelPath, ".md");
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
407
|
+
const chunkRows = [];
|
|
408
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
409
|
+
const chunk = chunks[i];
|
|
410
|
+
const id = chunkId(vaultProjectId, canonical.vaultRelPath, i, chunk.startLine, chunk.endLine);
|
|
411
|
+
chunkRows.push({
|
|
412
|
+
id,
|
|
413
|
+
projectId: vaultProjectId,
|
|
414
|
+
source: "vault",
|
|
415
|
+
tier: "topic",
|
|
416
|
+
path: canonical.vaultRelPath,
|
|
417
|
+
startLine: chunk.startLine,
|
|
418
|
+
endLine: chunk.endLine,
|
|
419
|
+
hash: chunk.hash,
|
|
420
|
+
text: chunk.text,
|
|
421
|
+
updatedAt
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (chunkRows.length > 0) await backend.insertChunks(chunkRows);
|
|
425
|
+
const vaultFileRow = {
|
|
426
|
+
vaultPath: canonical.vaultRelPath,
|
|
427
|
+
inode: canonical.inode,
|
|
428
|
+
device: canonical.device,
|
|
429
|
+
hash,
|
|
430
|
+
title,
|
|
431
|
+
indexedAt: updatedAt
|
|
432
|
+
};
|
|
433
|
+
await backend.upsertVaultFile(vaultFileRow);
|
|
431
434
|
result.filesIndexed++;
|
|
432
435
|
result.chunksCreated += chunks.length;
|
|
433
436
|
}
|
|
434
437
|
await yieldToEventLoop();
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
438
|
+
const allAliases = [];
|
|
439
|
+
for (const group of inodeGroups) for (const alias of group.aliases) {
|
|
440
|
+
allAliases.push({
|
|
441
|
+
vaultPath: alias.vaultRelPath,
|
|
442
|
+
canonicalPath: group.canonical.vaultRelPath,
|
|
443
|
+
inode: alias.inode,
|
|
444
|
+
device: alias.device
|
|
445
|
+
});
|
|
446
|
+
result.aliasesRecorded++;
|
|
447
|
+
}
|
|
448
|
+
const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));
|
|
449
|
+
for (const canonPath of canonicalPaths) await backend.deleteVaultAliases(canonPath);
|
|
450
|
+
if (allAliases.length > 0) await backend.upsertVaultAliases(allAliases);
|
|
446
451
|
await yieldToEventLoop();
|
|
447
|
-
db.exec("DELETE FROM vault_name_index");
|
|
448
|
-
const insertNameIndex = db.prepare(`
|
|
449
|
-
INSERT OR REPLACE INTO vault_name_index (name, vault_path) VALUES (?, ?)
|
|
450
|
-
`);
|
|
451
|
-
const insertNameIndexTx = db.transaction((entries) => {
|
|
452
|
-
for (const [name, path] of entries) insertNameIndex.run(name, path);
|
|
453
|
-
});
|
|
454
452
|
const nameEntries = [];
|
|
455
|
-
for (const [name, paths] of nameIndex) for (const path of paths) nameEntries.push(
|
|
456
|
-
|
|
453
|
+
for (const [name, paths] of nameIndex) for (const path of paths) nameEntries.push({
|
|
454
|
+
name,
|
|
455
|
+
vaultPath: path
|
|
456
|
+
});
|
|
457
|
+
await backend.replaceNameIndex(nameEntries);
|
|
457
458
|
await yieldToEventLoop();
|
|
458
|
-
db.exec("DELETE FROM vault_links");
|
|
459
|
-
const insertLink = db.prepare(`
|
|
460
|
-
INSERT OR IGNORE INTO vault_links
|
|
461
|
-
(source_path, target_raw, target_path, link_type, line_number)
|
|
462
|
-
VALUES (?, ?, ?, ?, ?)
|
|
463
|
-
`);
|
|
464
459
|
const linkRows = [];
|
|
460
|
+
const allSourcePaths = [];
|
|
461
|
+
let linkParseYield = 0;
|
|
465
462
|
for (const group of inodeGroups) {
|
|
463
|
+
if (linkParseYield++ % VAULT_YIELD_EVERY === 0) await yieldToEventLoop();
|
|
466
464
|
const { canonical } = group;
|
|
465
|
+
allSourcePaths.push(canonical.vaultRelPath);
|
|
467
466
|
let content;
|
|
468
467
|
try {
|
|
469
468
|
content = readFileSync(canonical.absPath, "utf8");
|
|
@@ -477,52 +476,55 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
477
476
|
if (link.isMdLink) linkType = link.isEmbed ? "md-embed" : "md-link";
|
|
478
477
|
else linkType = link.isEmbed ? "embed" : "wikilink";
|
|
479
478
|
linkRows.push({
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
target,
|
|
479
|
+
sourcePath: canonical.vaultRelPath,
|
|
480
|
+
targetRaw: link.raw,
|
|
481
|
+
targetPath: target,
|
|
483
482
|
linkType,
|
|
484
483
|
lineNumber: link.lineNumber
|
|
485
484
|
});
|
|
486
485
|
}
|
|
487
486
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
487
|
+
const LINK_BATCH_SIZE = 500;
|
|
488
|
+
for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {
|
|
489
|
+
const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);
|
|
490
|
+
const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));
|
|
491
|
+
await backend.replaceLinksForSources(batchSources, batchLinks);
|
|
492
|
+
await yieldToEventLoop();
|
|
493
|
+
}
|
|
491
494
|
result.linksExtracted = linkRows.length;
|
|
492
|
-
result.deadLinksFound = linkRows.filter((r) => r.
|
|
495
|
+
result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;
|
|
493
496
|
await yieldToEventLoop();
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const upsertHealth = db.prepare(`
|
|
503
|
-
INSERT INTO vault_health
|
|
504
|
-
(vault_path, inbound_count, outbound_count, dead_link_count, is_orphan, computed_at)
|
|
505
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
506
|
-
ON CONFLICT(vault_path) DO UPDATE SET
|
|
507
|
-
inbound_count = excluded.inbound_count,
|
|
508
|
-
outbound_count = excluded.outbound_count,
|
|
509
|
-
dead_link_count = excluded.dead_link_count,
|
|
510
|
-
is_orphan = excluded.is_orphan,
|
|
511
|
-
computed_at = excluded.computed_at
|
|
512
|
-
`);
|
|
497
|
+
const outboundMap = /* @__PURE__ */ new Map();
|
|
498
|
+
const deadMap = /* @__PURE__ */ new Map();
|
|
499
|
+
const inboundMap = /* @__PURE__ */ new Map();
|
|
500
|
+
for (const row of linkRows) {
|
|
501
|
+
outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);
|
|
502
|
+
if (row.targetPath === null) deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);
|
|
503
|
+
else inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);
|
|
504
|
+
}
|
|
513
505
|
const computedAt = Date.now();
|
|
514
506
|
let orphanCount = 0;
|
|
515
|
-
|
|
516
|
-
|
|
507
|
+
const HEALTH_BATCH_SIZE = 500;
|
|
508
|
+
for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {
|
|
509
|
+
const healthRows = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE).map((group) => {
|
|
517
510
|
const path = group.canonical.vaultRelPath;
|
|
518
511
|
const inbound = inboundMap.get(path) ?? 0;
|
|
519
512
|
const outbound = outboundMap.get(path) ?? 0;
|
|
520
513
|
const dead = deadMap.get(path) ?? 0;
|
|
521
|
-
const isOrphan = inbound === 0
|
|
514
|
+
const isOrphan = inbound === 0;
|
|
522
515
|
if (isOrphan) orphanCount++;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
516
|
+
return {
|
|
517
|
+
vaultPath: path,
|
|
518
|
+
inboundCount: inbound,
|
|
519
|
+
outboundCount: outbound,
|
|
520
|
+
deadLinkCount: dead,
|
|
521
|
+
isOrphan,
|
|
522
|
+
computedAt
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
await backend.upsertVaultHealth(healthRows);
|
|
526
|
+
await yieldToEventLoop();
|
|
527
|
+
}
|
|
526
528
|
result.orphansFound = orphanCount;
|
|
527
529
|
result.elapsed = Date.now() - startTime;
|
|
528
530
|
return result;
|
|
@@ -530,4 +532,4 @@ async function indexVault(db, vaultProjectId, vaultRoot) {
|
|
|
530
532
|
|
|
531
533
|
//#endregion
|
|
532
534
|
export { indexVault };
|
|
533
|
-
//# sourceMappingURL=vault-indexer-
|
|
535
|
+
//# sourceMappingURL=vault-indexer-Bi2cRmn7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-indexer-Bi2cRmn7.mjs","names":[],"sources":["../src/memory/vault/walk.ts","../src/memory/vault/deduplicate.ts","../src/memory/vault/parse-links.ts","../src/memory/vault/name-index.ts","../src/memory/vault/resolve.ts","../src/memory/vault/indexer.ts"],"sourcesContent":["/**\n * Vault directory walker — follows symlinks with cycle detection.\n */\n\nimport { statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/** Maximum number of .md files to collect from a vault. */\nconst VAULT_MAX_FILES = 10_000;\n\n/** Maximum recursion depth for vault directory walks. */\nconst VAULT_MAX_DEPTH = 10;\n\n/**\n * Directories to always skip, at any depth, during vault walks.\n * Includes standard build/VCS noise plus Obsidian-specific directories.\n */\nconst VAULT_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\",\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\",\n \".next\",\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Obsidian internals\n \".obsidian\",\n \".trash\",\n]);\n\n/**\n * Recursively collect all .md files under a vault root, following symlinks.\n *\n * Symlink-following behaviour:\n * - Symbolic links to files: followed if the target is a .md file\n * - Symbolic links to directories: followed with cycle detection via inode\n *\n * Cycle detection is based on the real inode of each visited directory.\n * Using the real stat (not lstat) ensures that symlinked dirs resolve to\n * their actual inode, preventing infinite loops.\n *\n * @param vaultRoot Absolute path to the vault root directory.\n * @param opts Optional overrides for maxFiles and maxDepth.\n */\nexport function walkVaultMdFiles(\n vaultRoot: string,\n opts?: { maxFiles?: number; maxDepth?: number },\n): VaultFile[] {\n const maxFiles = opts?.maxFiles ?? VAULT_MAX_FILES;\n const maxDepth = opts?.maxDepth ?? VAULT_MAX_DEPTH;\n\n const results: VaultFile[] = [];\n const visitedDirs = new Set<string>();\n\n function walk(dir: string, depth: number): void {\n if (results.length >= maxFiles) return;\n if (depth > maxDepth) return;\n\n // Get the real inode of this directory (follows symlinks on the dir itself)\n let dirStat: ReturnType<typeof statSync>;\n try {\n dirStat = statSync(dir);\n } catch {\n return; // Unreadable or broken symlink — skip\n }\n\n const dirKey = `${dirStat.dev}:${dirStat.ino}`;\n if (visitedDirs.has(dirKey)) return; // Cycle detected\n visitedDirs.add(dirKey);\n\n let entries: import(\"node:fs\").Dirent<string>[];\n try {\n entries = readdirSync(dir, { withFileTypes: true, encoding: \"utf8\" });\n } catch {\n return; // Unreadable directory — skip\n }\n\n for (const entry of entries) {\n if (results.length >= maxFiles) break;\n if (VAULT_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(dir, entry.name);\n\n if (entry.isSymbolicLink()) {\n // Follow the symlink — resolve to real target\n let targetStat: ReturnType<typeof statSync>;\n try {\n targetStat = statSync(full); // statSync follows symlinks\n } catch {\n continue; // Broken symlink — skip\n }\n\n if (targetStat.isDirectory()) {\n if (!VAULT_SKIP_DIRS.has(entry.name)) {\n walk(full, depth + 1);\n }\n } else if (targetStat.isFile() && entry.name.endsWith(\".md\")) {\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: targetStat.ino,\n device: targetStat.dev,\n });\n }\n } else if (entry.isDirectory()) {\n walk(full, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n let fileStat: ReturnType<typeof statSync>;\n try {\n fileStat = statSync(full);\n } catch {\n continue;\n }\n results.push({\n absPath: full,\n vaultRelPath: relative(vaultRoot, full),\n inode: fileStat.ino,\n device: fileStat.dev,\n });\n }\n }\n }\n\n if (existsSync(vaultRoot)) {\n walk(vaultRoot, 0);\n }\n\n return results;\n}\n","/**\n * Inode-based deduplication for vault files.\n */\n\nimport type { VaultFile, InodeGroup } from \"./types.js\";\n\n/**\n * Group vault files by inode identity (device + inode).\n *\n * Within each group, the canonical file is chosen as the one with the\n * fewest path separators (shallowest), breaking ties by shortest string.\n * All other group members become aliases.\n */\nexport function deduplicateByInode(files: VaultFile[]): InodeGroup[] {\n const groups = new Map<string, VaultFile[]>();\n\n for (const file of files) {\n const key = `${file.device}:${file.inode}`;\n const existing = groups.get(key);\n if (existing) {\n existing.push(file);\n } else {\n groups.set(key, [file]);\n }\n }\n\n const result: InodeGroup[] = [];\n\n for (const group of groups.values()) {\n if (group.length === 0) continue;\n\n // Sort: fewest path separators first, then shortest string\n const sorted = [...group].sort((a, b) => {\n const aDepth = (a.vaultRelPath.match(/\\//g) ?? []).length;\n const bDepth = (b.vaultRelPath.match(/\\//g) ?? []).length;\n if (aDepth !== bDepth) return aDepth - bDepth;\n return a.vaultRelPath.length - b.vaultRelPath.length;\n });\n\n const [canonical, ...aliases] = sorted as [VaultFile, ...VaultFile[]];\n result.push({ canonical, aliases });\n }\n\n return result;\n}\n","/**\n * Markdown link parser for vault files.\n *\n * Handles wikilinks ([[target]]), markdown links ([text](path)),\n * embeds (![[target]]), and frontmatter wikilinks.\n */\n\nimport type { ParsedLink } from \"./types.js\";\n\n/**\n * Parse all links from markdown content.\n *\n * Handles:\n * - Standard wikilinks: [[Target Note]]\n * - Aliased wikilinks: [[Target Note|Display Text]]\n * - Heading anchors: [[Target Note#Heading]] (stripped for resolution)\n * - Embeds: ![[Target Note]]\n * - Frontmatter wikilinks (YAML between --- delimiters)\n * - Markdown links: [text](path/to/note.md)\n * - Markdown embeds: \n *\n * External URLs (http://, https://, mailto:, etc.) are excluded — only\n * relative paths are treated as vault links.\n *\n * @param content Raw markdown file content.\n * @returns Array of parsed links in document order.\n */\nexport function parseLinks(content: string): ParsedLink[] {\n const links: ParsedLink[] = [];\n const lines = content.split(\"\\n\");\n\n // Determine frontmatter range (YAML between opening and closing ---)\n let frontmatterEnd = 0;\n if (content.startsWith(\"---\")) {\n const closingIdx = content.indexOf(\"\\n---\", 3);\n if (closingIdx !== -1) {\n frontmatterEnd = content.slice(0, closingIdx + 4).split(\"\\n\").length - 1;\n }\n }\n\n // Regex for [[wikilinks]] and ![[embeds]]\n const wikilinkRe = /(!?)\\[\\[([^\\]]+?)\\]\\]/g;\n\n // Regex for markdown links [text](target) and embeds \n const mdLinkRe = /(!)?\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx]!;\n const lineNumber = lineIdx + 1; // 1-indexed\n const isFrontmatter = lineIdx < frontmatterEnd;\n\n // --- Wikilinks ---\n wikilinkRe.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = wikilinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const inner = match[2]!;\n\n // Split on first | for alias\n const pipeIdx = inner.indexOf(\"|\");\n const beforePipe = pipeIdx === -1 ? inner : inner.slice(0, pipeIdx);\n const alias = pipeIdx === -1 ? null : inner.slice(pipeIdx + 1);\n\n // Strip heading anchor (everything after #)\n const hashIdx = beforePipe.indexOf(\"#\");\n const raw = hashIdx === -1 ? beforePipe.trim() : beforePipe.slice(0, hashIdx).trim();\n\n if (!raw) continue; // Skip links with empty targets (e.g. [[#Heading]])\n\n links.push({\n raw,\n alias: alias?.trim() ?? null,\n lineNumber,\n isEmbed: isEmbed && !isFrontmatter,\n isMdLink: false,\n });\n }\n\n // --- Markdown links --- (skip inside frontmatter)\n if (!isFrontmatter) {\n mdLinkRe.lastIndex = 0;\n while ((match = mdLinkRe.exec(line)) !== null) {\n const isEmbed = match[1] === \"!\";\n const displayText = match[2]!;\n let target = match[3]!.trim();\n\n // Skip external URLs\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) continue;\n\n // Skip pure anchor links (#heading)\n if (target.startsWith(\"#\")) continue;\n\n // Strip heading anchor from target\n const hashIdx = target.indexOf(\"#\");\n if (hashIdx !== -1) target = target.slice(0, hashIdx);\n\n // URL-decode (Obsidian encodes spaces as %20 in md links)\n try {\n target = decodeURIComponent(target);\n } catch {\n // Malformed encoding — use as-is\n }\n\n // Strip .md extension for resolution (resolveWikilink adds it back)\n const raw = target.replace(/\\.md$/i, \"\").trim();\n if (!raw) continue;\n\n links.push({\n raw,\n alias: displayText || null,\n lineNumber,\n isEmbed,\n isMdLink: true,\n });\n }\n }\n }\n\n return links;\n}\n\n/** @deprecated Use {@link parseLinks} instead. */\nexport const parseWikilinks = parseLinks;\n","/**\n * Name index builder for Obsidian wikilink resolution.\n */\n\nimport { basename } from \"node:path\";\nimport type { VaultFile } from \"./types.js\";\n\n/**\n * Build a name lookup index for Obsidian wikilink resolution.\n *\n * Maps lowercase filename (without .md extension) to all vault-relative paths\n * that share that name. Includes both canonical paths and alias paths so that\n * wikilinks resolve regardless of which path the file is accessed through.\n */\nexport function buildNameIndex(files: VaultFile[]): Map<string, string[]> {\n const index = new Map<string, string[]>();\n\n for (const file of files) {\n const name = basename(file.vaultRelPath, \".md\").toLowerCase();\n const existing = index.get(name);\n if (existing) {\n existing.push(file.vaultRelPath);\n } else {\n index.set(name, [file.vaultRelPath]);\n }\n }\n\n return index;\n}\n","/**\n * Wikilink resolver using Obsidian's shortest-match algorithm.\n */\n\nimport { normalize, basename, dirname } from \"node:path\";\n\n/**\n * Resolve a wikilink target to a vault-relative path using Obsidian's rules.\n *\n * Resolution algorithm:\n * 1. If raw contains \"/\", attempt exact path match (with and without .md).\n * 2. Normalize: lowercase the raw target, strip .md extension.\n * 3. Look up in the name index (all files with that basename).\n * 4. If exactly one match, return it.\n * 5. If multiple matches, pick the one closest to the source file\n * (longest common directory prefix, then shortest overall path).\n * 6. If no matches, return null (dead link).\n *\n * @param raw The raw link target (heading-stripped, pipe-stripped).\n * @param nameIndex Map from lowercase basename-without-ext to vault paths.\n * @param sourcePath Vault-relative path of the file containing the link.\n * @returns Vault-relative path of the resolved target, or null.\n */\nexport function resolveWikilink(\n raw: string,\n nameIndex: Map<string, string[]>,\n sourcePath: string,\n): string | null {\n if (!raw) return null;\n\n // Case 1: path contains \"/\" — try exact match with and without .md\n if (raw.includes(\"/\")) {\n const normalized = normalize(raw);\n const normalizedMd = normalized.endsWith(\".md\") ? normalized : normalized + \".md\";\n\n // Check if any indexed path matches (case-insensitive for macOS compatibility)\n for (const [, paths] of nameIndex) {\n for (const p of paths) {\n if (p === normalizedMd || p === normalized) return p;\n if (p.toLowerCase() === normalizedMd.toLowerCase()) return p;\n }\n }\n // Fall through to name lookup in case the path prefix was wrong\n }\n\n // Normalize the raw target for name lookup.\n // Use the basename only — Obsidian resolves by filename, not full path.\n const rawBase = basename(raw)\n .replace(/\\.md$/i, \"\")\n .toLowerCase()\n .trim();\n\n if (!rawBase) return null;\n\n const candidates = nameIndex.get(rawBase);\n\n if (!candidates || candidates.length === 0) {\n return null; // Dead link\n }\n\n if (candidates.length === 1) {\n return candidates[0]!;\n }\n\n // Multiple matches — pick the one closest to the source file\n const sourceDir = dirname(sourcePath);\n\n let bestPath: string | null = null;\n let bestPrefixLen = -1;\n let bestPathLen = Infinity;\n\n for (const candidate of candidates) {\n const candidateDir = dirname(candidate);\n const prefixLen = commonPrefixLength(sourceDir, candidateDir);\n const pathLen = candidate.length;\n\n if (\n prefixLen > bestPrefixLen ||\n (prefixLen === bestPrefixLen && pathLen < bestPathLen)\n ) {\n bestPrefixLen = prefixLen;\n bestPathLen = pathLen;\n bestPath = candidate;\n }\n }\n\n return bestPath;\n}\n\n/**\n * Compute the length of the common prefix between two directory paths,\n * measured in path segments (not raw characters).\n *\n * Example: \"a/b/c\" and \"a/b/d\" → 2 (common: \"a\", \"b\")\n */\nfunction commonPrefixLength(a: string, b: string): number {\n if (a === \".\" && b === \".\") return 0;\n const aParts = a === \".\" ? [] : a.split(\"/\");\n const bParts = b === \".\" ? [] : b.split(\"/\");\n let count = 0;\n const len = Math.min(aParts.length, bParts.length);\n for (let i = 0; i < len; i++) {\n if (aParts[i] === bParts[i]) {\n count++;\n } else {\n break;\n }\n }\n return count;\n}\n","/**\n * Main vault indexing orchestrator.\n *\n * Indexes an entire Obsidian vault (or any markdown knowledge base), following\n * symlinks, deduplicating files by inode, parsing wikilinks, and computing\n * per-file health metrics (orphan detection, dead links).\n *\n * Key differences from the project indexer (indexer/sync.ts):\n * - Follows symbolic links (project indexer skips them)\n * - Deduplicates files with the same inode (same content reachable via multiple paths)\n * - Parses [[wikilinks]] and builds a directed link graph\n * - Resolves wikilinks using Obsidian's shortest-match algorithm\n * - Computes health metrics per file: inbound/outbound link counts, dead links, orphans\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport type {\n StorageBackend,\n VaultFileRow,\n VaultAliasRow,\n VaultLinkRow,\n VaultHealthRow,\n VaultNameEntry,\n ChunkRow,\n} from \"../../storage/interface.js\";\nimport { chunkMarkdown } from \"../chunker.js\";\nimport { sha256File, chunkId, yieldToEventLoop } from \"../indexer/helpers.js\";\nimport { walkVaultMdFiles } from \"./walk.js\";\nimport { deduplicateByInode } from \"./deduplicate.js\";\nimport { parseLinks } from \"./parse-links.js\";\nimport { buildNameIndex } from \"./name-index.js\";\nimport { resolveWikilink } from \"./resolve.js\";\nimport type { VaultIndexResult } from \"./types.js\";\n\n/** Number of files to process before yielding to the event loop. */\nconst VAULT_YIELD_EVERY = 1;\n\n/**\n * Index an entire Obsidian vault (or markdown knowledge base) using the\n * async StorageBackend interface.\n *\n * Steps:\n * 1. Walk vault root, following symlinks.\n * 2. Deduplicate by inode — each unique file is indexed once.\n * 3. Build a name index for wikilink resolution.\n * 4. For each canonical file:\n * a. SHA-256 hash for change detection — skip unchanged files.\n * b. Read content, chunk with chunkMarkdown().\n * c. Insert chunks into backend (memory_chunks and memory_fts).\n * d. Upsert vault_files row.\n * 5. Record aliases in vault_aliases.\n * 6. Rebuild vault_name_index table.\n * 7. Rebuild vault_links:\n * a. Parse [[wikilinks]] from each canonical file.\n * b. Resolve each link with resolveWikilink().\n * c. Insert into vault_links.\n * 8. Compute and upsert health metrics (vault_health).\n * 9. Return statistics.\n *\n * @param backend StorageBackend to write to.\n * @param vaultProjectId Registry project ID for the vault \"project\".\n * @param vaultRoot Absolute path to the vault root directory.\n */\nexport async function indexVault(\n backend: StorageBackend,\n vaultProjectId: number,\n vaultRoot: string,\n): Promise<VaultIndexResult> {\n const startTime = Date.now();\n\n const result: VaultIndexResult = {\n filesIndexed: 0,\n chunksCreated: 0,\n filesSkipped: 0,\n aliasesRecorded: 0,\n linksExtracted: 0,\n deadLinksFound: 0,\n orphansFound: 0,\n elapsed: 0,\n };\n\n // Step 1: Walk vault, collecting all .md files (follows symlinks)\n const allFiles = walkVaultMdFiles(vaultRoot);\n\n // Step 2: Deduplicate by inode\n const inodeGroups = deduplicateByInode(allFiles);\n\n // Step 3: Build name index (from all files including aliases, for resolution)\n const nameIndex = buildNameIndex(allFiles);\n\n // Step 4: Index each canonical file\n await yieldToEventLoop();\n let filesSinceYield = 0;\n\n for (const group of inodeGroups) {\n if (filesSinceYield >= VAULT_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const { canonical } = group;\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n result.filesSkipped++;\n continue;\n }\n\n const hash = sha256File(content);\n\n // Change detection: skip if hash is unchanged\n const existing = await backend.getVaultFile(canonical.vaultRelPath);\n if (existing?.hash === hash) {\n result.filesSkipped++;\n continue;\n }\n\n // Delete old chunks for this vault path\n await backend.deleteChunksForFile(vaultProjectId, canonical.vaultRelPath);\n\n // Chunk the content\n const chunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n // Extract title from first H1 heading or filename\n const titleMatch = /^#\\s+(.+)$/m.exec(content);\n const title = titleMatch\n ? titleMatch[1]!.trim()\n : basename(canonical.vaultRelPath, \".md\");\n\n // Build chunk rows\n const chunkRows: ChunkRow[] = [];\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i]!;\n const id = chunkId(\n vaultProjectId,\n canonical.vaultRelPath,\n i,\n chunk.startLine,\n chunk.endLine,\n );\n chunkRows.push({\n id,\n projectId: vaultProjectId,\n source: \"vault\",\n tier: \"topic\",\n path: canonical.vaultRelPath,\n startLine: chunk.startLine,\n endLine: chunk.endLine,\n hash: chunk.hash,\n text: chunk.text,\n updatedAt,\n });\n }\n\n if (chunkRows.length > 0) {\n await backend.insertChunks(chunkRows);\n }\n\n // Upsert vault file record\n const vaultFileRow: VaultFileRow = {\n vaultPath: canonical.vaultRelPath,\n inode: canonical.inode,\n device: canonical.device,\n hash,\n title,\n indexedAt: updatedAt,\n };\n await backend.upsertVaultFile(vaultFileRow);\n\n result.filesIndexed++;\n result.chunksCreated += chunks.length;\n }\n\n // Step 5: Record aliases in vault_aliases\n await yieldToEventLoop();\n\n const allAliases: VaultAliasRow[] = [];\n for (const group of inodeGroups) {\n for (const alias of group.aliases) {\n allAliases.push({\n vaultPath: alias.vaultRelPath,\n canonicalPath: group.canonical.vaultRelPath,\n inode: alias.inode,\n device: alias.device,\n });\n result.aliasesRecorded++;\n }\n }\n\n const canonicalPaths = new Set(inodeGroups.map((g) => g.canonical.vaultRelPath));\n for (const canonPath of canonicalPaths) {\n await backend.deleteVaultAliases(canonPath);\n }\n if (allAliases.length > 0) {\n await backend.upsertVaultAliases(allAliases);\n }\n\n // Step 6: Rebuild vault_name_index\n await yieldToEventLoop();\n\n const nameEntries: VaultNameEntry[] = [];\n for (const [name, paths] of nameIndex) {\n for (const path of paths) {\n nameEntries.push({ name, vaultPath: path });\n }\n }\n await backend.replaceNameIndex(nameEntries);\n\n // Step 7: Rebuild vault_links\n await yieldToEventLoop();\n\n const linkRows: VaultLinkRow[] = [];\n const allSourcePaths: string[] = [];\n\n let linkParseYield = 0;\n for (const group of inodeGroups) {\n if (linkParseYield++ % VAULT_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const { canonical } = group;\n allSourcePaths.push(canonical.vaultRelPath);\n\n let content: string;\n try {\n content = readFileSync(canonical.absPath, \"utf8\");\n } catch {\n continue;\n }\n\n const parsedLinks = parseLinks(content);\n for (const link of parsedLinks) {\n const target = resolveWikilink(link.raw, nameIndex, canonical.vaultRelPath);\n let linkType: string;\n if (link.isMdLink) {\n linkType = link.isEmbed ? \"md-embed\" : \"md-link\";\n } else {\n linkType = link.isEmbed ? \"embed\" : \"wikilink\";\n }\n linkRows.push({\n sourcePath: canonical.vaultRelPath,\n targetRaw: link.raw,\n targetPath: target,\n linkType,\n lineNumber: link.lineNumber,\n });\n }\n }\n\n // Replace all links for all sources in batches of 500\n const LINK_BATCH_SIZE = 500;\n for (let i = 0; i < allSourcePaths.length; i += LINK_BATCH_SIZE) {\n const batchSources = allSourcePaths.slice(i, i + LINK_BATCH_SIZE);\n const batchLinks = linkRows.filter((r) => batchSources.includes(r.sourcePath));\n await backend.replaceLinksForSources(batchSources, batchLinks);\n await yieldToEventLoop();\n }\n\n result.linksExtracted = linkRows.length;\n result.deadLinksFound = linkRows.filter((r) => r.targetPath === null).length;\n\n // Step 8: Compute and upsert vault_health metrics\n await yieldToEventLoop();\n\n const outboundMap = new Map<string, number>();\n const deadMap = new Map<string, number>();\n const inboundMap = new Map<string, number>();\n\n for (const row of linkRows) {\n outboundMap.set(row.sourcePath, (outboundMap.get(row.sourcePath) ?? 0) + 1);\n if (row.targetPath === null) {\n deadMap.set(row.sourcePath, (deadMap.get(row.sourcePath) ?? 0) + 1);\n } else {\n inboundMap.set(row.targetPath, (inboundMap.get(row.targetPath) ?? 0) + 1);\n }\n }\n\n const computedAt = Date.now();\n let orphanCount = 0;\n\n const HEALTH_BATCH_SIZE = 500;\n for (let i = 0; i < inodeGroups.length; i += HEALTH_BATCH_SIZE) {\n const batch = inodeGroups.slice(i, i + HEALTH_BATCH_SIZE);\n const healthRows: VaultHealthRow[] = batch.map((group) => {\n const path = group.canonical.vaultRelPath;\n const inbound = inboundMap.get(path) ?? 0;\n const outbound = outboundMap.get(path) ?? 0;\n const dead = deadMap.get(path) ?? 0;\n const isOrphan = inbound === 0;\n if (isOrphan) orphanCount++;\n return {\n vaultPath: path,\n inboundCount: inbound,\n outboundCount: outbound,\n deadLinkCount: dead,\n isOrphan,\n computedAt,\n };\n });\n await backend.upsertVaultHealth(healthRows);\n await yieldToEventLoop();\n }\n\n result.orphansFound = orphanCount;\n result.elapsed = Date.now() - startTime;\n\n return result;\n}\n"],"mappings":"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"}
|