ctxman 3.1.1 → 3.2.7

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 (53) hide show
  1. package/.ctxman/llm-profiles.json +6 -6
  2. package/README.md +16 -10
  3. package/bin/cli.js +107 -8
  4. package/bin/cm-ask.js +64 -6
  5. package/bin/cm-audit.js +76 -0
  6. package/bin/cm-diagnose.js +199 -0
  7. package/bin/cm-focus.js +322 -0
  8. package/bin/cm-map.js +71 -18
  9. package/bin/cm-pack.js +36 -5
  10. package/bin/cm-policy.js +107 -2
  11. package/bin/cm-pr.js +18 -3
  12. package/bin/cm-search.js +84 -19
  13. package/bin/cm-task.js +192 -0
  14. package/bin/cm-who.js +119 -0
  15. package/bin/cm-why.js +41 -5
  16. package/index.d.ts +931 -1
  17. package/index.js +105 -1
  18. package/lib/analyzers/token-calculator.js +76 -4
  19. package/lib/api/mcp/server.js +618 -16
  20. package/lib/cache/CacheManager.js +161 -47
  21. package/lib/core/Analyzer.js +61 -5
  22. package/lib/diagnose/index.js +712 -0
  23. package/lib/focus/index.js +790 -0
  24. package/lib/map/call-graph.js +570 -0
  25. package/lib/map/graph.js +30 -5
  26. package/lib/map/index.js +12 -4
  27. package/lib/map/rank.js +45 -7
  28. package/lib/map/render.js +89 -28
  29. package/lib/pack/index.js +20 -1
  30. package/lib/pack/select.js +8 -1
  31. package/lib/policy/audit.js +3 -1
  32. package/lib/policy/index.js +5 -0
  33. package/lib/policy/workspace.js +254 -0
  34. package/lib/pr/pr-context.js +292 -31
  35. package/lib/pr/pr-renderer.js +25 -10
  36. package/lib/rag/EmbeddingProvider.js +139 -2
  37. package/lib/rag/VectorStoreFactory.js +10 -4
  38. package/lib/rag/ask.js +186 -17
  39. package/lib/rag/stores/SqliteVectorStore.js +595 -0
  40. package/lib/search/hybrid.js +324 -0
  41. package/lib/search/symbols.js +75 -11
  42. package/lib/task/index.js +630 -0
  43. package/lib/utils/file-utils.js +1 -0
  44. package/lib/utils/multi-repo-manager.js +4 -1
  45. package/lib/watch/FileWatcher.js +121 -31
  46. package/lib/watch/IncrementalAnalyzer.js +964 -16
  47. package/lib/who/index.js +893 -0
  48. package/lib/why/analyzer-bridge.js +223 -0
  49. package/lib/why/discussions.js +314 -0
  50. package/lib/why/index.js +356 -30
  51. package/lib/why/render.js +154 -9
  52. package/lib/workspace/index.js +1274 -0
  53. package/package.json +1 -1
@@ -0,0 +1,595 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { createRequire } from 'module';
5
+ import { BaseVectorStore } from './BaseVectorStore.js';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ let cachedDatabaseSync = null;
9
+
10
+ /**
11
+ * Lazily loads `DatabaseSync` from `node:sqlite` while suppressing Node's
12
+ * one-time `ExperimentalWarning: SQLite is an experimental feature` on stderr.
13
+ */
14
+ function getDatabaseSync() {
15
+ if (cachedDatabaseSync) return cachedDatabaseSync;
16
+ const origEmitWarning = process.emitWarning;
17
+ process.emitWarning = function (warning, ...args) {
18
+ const msg = typeof warning === 'string' ? warning : warning?.message || '';
19
+ if (/SQLite is an experimental feature/i.test(msg)) return;
20
+ return origEmitWarning.call(this, warning, ...args);
21
+ };
22
+ try {
23
+ ({ DatabaseSync: cachedDatabaseSync } = require('node:sqlite'));
24
+ return cachedDatabaseSync;
25
+ } finally {
26
+ process.emitWarning = origEmitWarning;
27
+ }
28
+ }
29
+
30
+ const toPosix = (p) =>
31
+ String(p ?? '')
32
+ .split(path.sep)
33
+ .join('/');
34
+
35
+ /** Byte-order comparison, identical across platforms and locales */
36
+ function comparePaths(a, b) {
37
+ return a < b ? -1 : a > b ? 1 : 0;
38
+ }
39
+
40
+ /**
41
+ * Zero-native-binary vector & knowledge-graph store using Node 22+ built-in node:sqlite (`DatabaseSync`).
42
+ * Stores document chunks, content SHA-256 hashes, metadata, float32 embedding vectors,
43
+ * and incremental repository knowledge-graph nodes & edges in `<storePath>/index.sqlite`.
44
+ * Supports incremental upserts (skipping unchanged content), document removal,
45
+ * graph delta synchronization, and exact cosine similarity vector search.
46
+ */
47
+ export class SqliteVectorStore extends BaseVectorStore {
48
+ /**
49
+ * @param {import('../EmbeddingProvider').EmbeddingProvider} embeddingProvider
50
+ * @param {string} [storagePath='.ctxman/self-knowledge'] - Directory or .sqlite file path
51
+ */
52
+ constructor(embeddingProvider, storagePath = '.ctxman/self-knowledge') {
53
+ super(embeddingProvider);
54
+ this.storagePath = storagePath;
55
+ this.dbPath = storagePath.endsWith('.sqlite')
56
+ ? storagePath
57
+ : path.join(storagePath, 'index.sqlite');
58
+ this.db = null;
59
+ this.initialized = false;
60
+ }
61
+
62
+ /**
63
+ * Initialize directory, SQLite database, documents table, and knowledge-graph tables.
64
+ */
65
+ async init() {
66
+ if (this.initialized) return;
67
+
68
+ const dir = path.dirname(this.dbPath);
69
+ if (!fs.existsSync(dir)) {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ }
72
+
73
+ const DatabaseSync = getDatabaseSync();
74
+ this.db = new DatabaseSync(this.dbPath);
75
+
76
+ // Create tables for documents (vectors) and knowledge graph (nodes + edges)
77
+ this.db.exec(`
78
+ CREATE TABLE IF NOT EXISTS documents (
79
+ id TEXT PRIMARY KEY,
80
+ file_path TEXT,
81
+ content_hash TEXT NOT NULL,
82
+ text TEXT NOT NULL,
83
+ metadata TEXT,
84
+ vector BLOB,
85
+ updated_at INTEGER
86
+ );
87
+ CREATE INDEX IF NOT EXISTS idx_documents_file_path ON documents(file_path);
88
+ CREATE INDEX IF NOT EXISTS idx_documents_content_hash ON documents(content_hash);
89
+
90
+ CREATE TABLE IF NOT EXISTS graph_nodes (
91
+ path TEXT PRIMARY KEY,
92
+ language TEXT NOT NULL,
93
+ bytes INTEGER NOT NULL DEFAULT 0,
94
+ content_hash TEXT,
95
+ methods TEXT NOT NULL DEFAULT '[]',
96
+ declarations TEXT NOT NULL DEFAULT '[]',
97
+ imports TEXT NOT NULL DEFAULT '[]',
98
+ symbol_refs REAL NOT NULL DEFAULT 0,
99
+ unresolved_imports INTEGER NOT NULL DEFAULT 0,
100
+ updated_at INTEGER
101
+ );
102
+
103
+ CREATE TABLE IF NOT EXISTS graph_edges (
104
+ from_path TEXT NOT NULL,
105
+ to_path TEXT NOT NULL,
106
+ weight REAL NOT NULL DEFAULT 1,
107
+ updated_at INTEGER,
108
+ PRIMARY KEY (from_path, to_path)
109
+ );
110
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_from ON graph_edges(from_path);
111
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_to ON graph_edges(to_path);
112
+ `);
113
+
114
+ this.stmtGetById = this.db.prepare('SELECT id, content_hash FROM documents WHERE id = ?');
115
+ this.stmtGetByPath = this.db.prepare(
116
+ 'SELECT id, content_hash FROM documents WHERE file_path = ?'
117
+ );
118
+ this.stmtUpsert = this.db.prepare(`
119
+ INSERT INTO documents (id, file_path, content_hash, text, metadata, vector, updated_at)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?)
121
+ ON CONFLICT(id) DO UPDATE SET
122
+ file_path = excluded.file_path,
123
+ content_hash = excluded.content_hash,
124
+ text = excluded.text,
125
+ metadata = excluded.metadata,
126
+ vector = excluded.vector,
127
+ updated_at = excluded.updated_at
128
+ `);
129
+ this.stmtDeleteById = this.db.prepare('DELETE FROM documents WHERE id = ?');
130
+ this.stmtAllMeta = this.db.prepare('SELECT id, file_path, metadata FROM documents');
131
+ this.stmtAllVectors = this.db.prepare(
132
+ 'SELECT id, file_path, text, metadata, vector FROM documents'
133
+ );
134
+ this.stmtCount = this.db.prepare('SELECT COUNT(*) AS count FROM documents');
135
+
136
+ // Graph prepared statements
137
+ this.stmtUpsertNode = this.db.prepare(`
138
+ INSERT INTO graph_nodes (
139
+ path, language, bytes, content_hash, methods, declarations, imports,
140
+ symbol_refs, unresolved_imports, updated_at
141
+ )
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
143
+ ON CONFLICT(path) DO UPDATE SET
144
+ language = excluded.language,
145
+ bytes = excluded.bytes,
146
+ content_hash = excluded.content_hash,
147
+ methods = excluded.methods,
148
+ declarations = excluded.declarations,
149
+ imports = excluded.imports,
150
+ symbol_refs = excluded.symbol_refs,
151
+ unresolved_imports = excluded.unresolved_imports,
152
+ updated_at = excluded.updated_at
153
+ `);
154
+ this.stmtDeleteNode = this.db.prepare('DELETE FROM graph_nodes WHERE path = ?');
155
+ this.stmtDeleteOutEdges = this.db.prepare('DELETE FROM graph_edges WHERE from_path = ?');
156
+ this.stmtDeleteIncidentEdges = this.db.prepare(
157
+ 'DELETE FROM graph_edges WHERE from_path = ? OR to_path = ?'
158
+ );
159
+ this.stmtUpsertEdge = this.db.prepare(`
160
+ INSERT INTO graph_edges (from_path, to_path, weight, updated_at)
161
+ VALUES (?, ?, ?, ?)
162
+ ON CONFLICT(from_path, to_path) DO UPDATE SET
163
+ weight = excluded.weight,
164
+ updated_at = excluded.updated_at
165
+ `);
166
+ this.stmtAllNodes = this.db.prepare(`
167
+ SELECT path, language, bytes, content_hash, methods, declarations, imports,
168
+ symbol_refs, unresolved_imports, updated_at
169
+ FROM graph_nodes
170
+ ORDER BY path ASC
171
+ `);
172
+ this.stmtAllEdges = this.db.prepare(`
173
+ SELECT from_path, to_path, weight
174
+ FROM graph_edges
175
+ ORDER BY from_path ASC, to_path ASC
176
+ `);
177
+ this.stmtNodeCount = this.db.prepare('SELECT COUNT(*) AS count FROM graph_nodes');
178
+ this.stmtEdgeCount = this.db.prepare('SELECT COUNT(*) AS count FROM graph_edges');
179
+
180
+ this.initialized = true;
181
+ }
182
+
183
+ /**
184
+ * Add a single document to the store.
185
+ * @param {string} text
186
+ * @param {Object} [metadata={}]
187
+ * @returns {Promise<{upserted: number, skipped: number}>}
188
+ */
189
+ async addDocument(text, metadata = {}) {
190
+ return this.addDocuments([{ text, metadata }]);
191
+ }
192
+
193
+ /**
194
+ * Incremental upsert of documents.
195
+ * Skips re-embedding and rewriting if content SHA-256 is unchanged.
196
+ * @param {Array<{text: string, id?: string, metadata?: Object}>} docs
197
+ * @returns {Promise<{upserted: number, skipped: number}>}
198
+ */
199
+ async addDocuments(docs) {
200
+ if (!docs || docs.length === 0) return { upserted: 0, skipped: 0 };
201
+ if (!this.initialized) await this.init();
202
+
203
+ let upserted = 0;
204
+ let skipped = 0;
205
+
206
+ for (const doc of docs) {
207
+ const text = String(doc.text ?? '');
208
+ const contentHash = crypto.createHash('sha256').update(text).digest('hex');
209
+
210
+ const meta = doc.metadata ?? {};
211
+ const filePath =
212
+ meta.path ?? (typeof doc.id === 'string' && !doc.id.includes(':') ? doc.id : null);
213
+ const id = doc.id ?? meta.sourceId ?? (filePath ? filePath : crypto.randomUUID());
214
+
215
+ // Check if unchanged by id or filePath
216
+ const existing =
217
+ this.stmtGetById.get(id) ?? (filePath ? this.stmtGetByPath.get(filePath) : null);
218
+ if (existing && existing.content_hash === contentHash) {
219
+ // Content hash matches; skip re-embedding
220
+ skipped++;
221
+ continue;
222
+ }
223
+
224
+ const targetId = existing ? existing.id : id;
225
+
226
+ // Compute vector embedding
227
+ const vector = await this.embeddingProvider.embed(text);
228
+ const float32 = new Float32Array(vector);
229
+ const blob = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
230
+
231
+ this.stmtUpsert.run(
232
+ targetId,
233
+ filePath,
234
+ contentHash,
235
+ text,
236
+ JSON.stringify(meta),
237
+ blob,
238
+ Date.now()
239
+ );
240
+ upserted++;
241
+ }
242
+
243
+ return { upserted, skipped };
244
+ }
245
+
246
+ /**
247
+ * Remove document(s) matching a document ID, relative path, or absolute file path.
248
+ * @param {string} idOrFilePath
249
+ * @returns {Promise<number>} Number of removed documents
250
+ */
251
+ async removeDocument(idOrFilePath) {
252
+ if (!idOrFilePath) return 0;
253
+ if (!this.initialized) await this.init();
254
+
255
+ const target = String(idOrFilePath);
256
+ const posixTarget = toPosix(target);
257
+ const suffixTarget = posixTarget.startsWith('/') ? posixTarget : `/${posixTarget}`;
258
+
259
+ const rows = this.stmtAllMeta.all();
260
+ const idsToDelete = new Set();
261
+
262
+ for (const row of rows) {
263
+ if (row.id === target || row.id === posixTarget) {
264
+ idsToDelete.add(row.id);
265
+ continue;
266
+ }
267
+ if (row.file_path) {
268
+ const posixFilePath = toPosix(row.file_path);
269
+ if (
270
+ row.file_path === target ||
271
+ posixFilePath === posixTarget ||
272
+ posixFilePath.endsWith(suffixTarget)
273
+ ) {
274
+ idsToDelete.add(row.id);
275
+ continue;
276
+ }
277
+ }
278
+ if (row.metadata) {
279
+ try {
280
+ const meta = JSON.parse(row.metadata);
281
+ const metaPath = meta.path ? toPosix(meta.path) : null;
282
+ const metaRel = meta.relativePath ? toPosix(meta.relativePath) : null;
283
+ if (
284
+ metaPath === posixTarget ||
285
+ (metaPath && metaPath.endsWith(suffixTarget)) ||
286
+ metaRel === posixTarget
287
+ ) {
288
+ idsToDelete.add(row.id);
289
+ }
290
+ } catch {
291
+ // ignore malformed metadata
292
+ }
293
+ }
294
+ }
295
+
296
+ let removed = 0;
297
+ for (const id of idsToDelete) {
298
+ const res = this.stmtDeleteById.run(id);
299
+ removed += Number(res?.changes ?? 1);
300
+ }
301
+ return removed;
302
+ }
303
+
304
+ /**
305
+ * Upsert a single repository knowledge-graph node.
306
+ * @param {Object} node
307
+ */
308
+ async upsertGraphNode(node) {
309
+ if (!node || !node.path) return;
310
+ if (!this.initialized) await this.init();
311
+
312
+ const relPath = toPosix(node.path);
313
+ this.stmtUpsertNode.run(
314
+ relPath,
315
+ String(node.language ?? 'unknown'),
316
+ Number(node.bytes ?? 0),
317
+ node.contentHash ?? null,
318
+ JSON.stringify(node.methods ?? []),
319
+ JSON.stringify(node.declarations ?? []),
320
+ JSON.stringify(node.imports ?? []),
321
+ Number(node.symbolRefs ?? 0),
322
+ Number(node.unresolvedImports ?? 0),
323
+ Date.now()
324
+ );
325
+ }
326
+
327
+ /**
328
+ * Replace all outgoing import edges for a single file node.
329
+ * @param {string} fromPath
330
+ * @param {Array<{to: string, weight?: number}>} edges
331
+ */
332
+ async replaceGraphEdges(fromPath, edges = []) {
333
+ if (!fromPath) return;
334
+ if (!this.initialized) await this.init();
335
+
336
+ const posixFrom = toPosix(fromPath);
337
+ const now = Date.now();
338
+ this.db.exec('BEGIN IMMEDIATE');
339
+ try {
340
+ this.stmtDeleteOutEdges.run(posixFrom);
341
+ for (const edge of edges) {
342
+ if (!edge || !edge.to) continue;
343
+ this.stmtUpsertEdge.run(posixFrom, toPosix(edge.to), Number(edge.weight ?? 1), now);
344
+ }
345
+ this.db.exec('COMMIT');
346
+ } catch (err) {
347
+ this.db.exec('ROLLBACK');
348
+ throw err;
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Remove a node and all incident (incoming + outgoing) edges from the knowledge graph.
354
+ * @param {string} filePath - Project-relative path
355
+ * @returns {Promise<{removedNode: number, removedEdges: number}>}
356
+ */
357
+ async removeGraphNode(filePath) {
358
+ if (!filePath) return { removedNode: 0, removedEdges: 0 };
359
+ if (!this.initialized) await this.init();
360
+
361
+ const posixPath = toPosix(filePath);
362
+ this.db.exec('BEGIN IMMEDIATE');
363
+ try {
364
+ const edgeRes = this.stmtDeleteIncidentEdges.run(posixPath, posixPath);
365
+ const nodeRes = this.stmtDeleteNode.run(posixPath);
366
+ this.db.exec('COMMIT');
367
+ return {
368
+ removedNode: Number(nodeRes?.changes ?? 0),
369
+ removedEdges: Number(edgeRes?.changes ?? 0),
370
+ };
371
+ } catch (err) {
372
+ this.db.exec('ROLLBACK');
373
+ throw err;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Synchronize a full or incremental set of nodes and edges in a single SQLite transaction.
379
+ * @param {{nodes: Array<Object>, edges: Array<{from: string, to: string, weight?: number}>, replaceAll?: boolean}} graph
380
+ */
381
+ async syncGraph(graph) {
382
+ if (!graph) return;
383
+ if (!this.initialized) await this.init();
384
+
385
+ const now = Date.now();
386
+ this.db.exec('BEGIN IMMEDIATE');
387
+ try {
388
+ if (graph.replaceAll) {
389
+ this.db.exec('DELETE FROM graph_nodes; DELETE FROM graph_edges;');
390
+ }
391
+ for (const node of graph.nodes ?? []) {
392
+ const relPath = toPosix(node.path);
393
+ this.stmtUpsertNode.run(
394
+ relPath,
395
+ String(node.language ?? 'unknown'),
396
+ Number(node.bytes ?? 0),
397
+ node.contentHash ?? null,
398
+ JSON.stringify(node.methods ?? []),
399
+ JSON.stringify(node.declarations ?? []),
400
+ JSON.stringify(node.imports ?? []),
401
+ Number(node.symbolRefs ?? 0),
402
+ Number(node.unresolvedImports ?? 0),
403
+ now
404
+ );
405
+ }
406
+ if (Array.isArray(graph.edges)) {
407
+ this.db.exec('DELETE FROM graph_edges;');
408
+ for (const edge of graph.edges) {
409
+ if (!edge || !edge.from || !edge.to) continue;
410
+ this.stmtUpsertEdge.run(
411
+ toPosix(edge.from),
412
+ toPosix(edge.to),
413
+ Number(edge.weight ?? 1),
414
+ now
415
+ );
416
+ }
417
+ }
418
+ this.db.exec('COMMIT');
419
+ } catch (err) {
420
+ this.db.exec('ROLLBACK');
421
+ throw err;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Read the persisted knowledge graph (`nodes` and `edges`) from SQLite.
427
+ * @returns {Promise<{nodes: Array<Object>, edges: Array<{from: string, to: string, weight: number}>}>}
428
+ */
429
+ async getGraph() {
430
+ if (!this.initialized) await this.init();
431
+
432
+ const rawNodes = this.stmtAllNodes.all();
433
+ const rawEdges = this.stmtAllEdges.all();
434
+
435
+ const parseJson = (str, fallback) => {
436
+ if (!str) return fallback;
437
+ try {
438
+ return JSON.parse(str);
439
+ } catch {
440
+ return fallback;
441
+ }
442
+ };
443
+
444
+ const nodes = rawNodes
445
+ .map((row) => ({
446
+ path: row.path,
447
+ language: row.language,
448
+ bytes: Number(row.bytes ?? 0),
449
+ contentHash: row.content_hash ?? null,
450
+ methods: parseJson(row.methods, []),
451
+ declarations: parseJson(row.declarations, []),
452
+ imports: parseJson(row.imports, []),
453
+ symbolRefs: Number(row.symbol_refs ?? 0),
454
+ unresolvedImports: Number(row.unresolved_imports ?? 0),
455
+ updatedAt: row.updated_at ? Number(row.updated_at) : null,
456
+ }))
457
+ .sort((a, b) => comparePaths(a.path, b.path));
458
+
459
+ const edges = rawEdges
460
+ .map((row) => ({
461
+ from: row.from_path,
462
+ to: row.to_path,
463
+ weight: Number(row.weight ?? 1),
464
+ }))
465
+ .sort((a, b) => comparePaths(a.from, b.from) || comparePaths(a.to, b.to));
466
+
467
+ return { nodes, edges };
468
+ }
469
+
470
+ /**
471
+ * Return node and edge counts in the persisted knowledge graph.
472
+ * @returns {{nodes: number, edges: number}}
473
+ */
474
+ graphCounts() {
475
+ if (!this.initialized || !this.db) return { nodes: 0, edges: 0 };
476
+ const nRow = this.stmtNodeCount.get();
477
+ const eRow = this.stmtEdgeCount.get();
478
+ return {
479
+ nodes: nRow ? Number(nRow.count) : 0,
480
+ edges: eRow ? Number(eRow.count) : 0,
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Exact cosine similarity search over stored documents.
486
+ * @param {string} queryText
487
+ * @param {number} [limit=5]
488
+ * @returns {Promise<Array<{id?: string, text: string, score: number, metadata: Object}>>}
489
+ */
490
+ async search(queryText, limit = 5) {
491
+ if (!this.initialized) await this.init();
492
+
493
+ const countRow = this.stmtCount.get();
494
+ if (!countRow || Number(countRow.count) === 0) return [];
495
+
496
+ const queryVector = await this.embeddingProvider.embed(queryText);
497
+ const qLen = queryVector.length;
498
+
499
+ let queryNormSq = 0;
500
+ for (let i = 0; i < qLen; i++) {
501
+ queryNormSq += queryVector[i] * queryVector[i];
502
+ }
503
+ const queryNorm = Math.sqrt(queryNormSq);
504
+ if (queryNorm === 0) return [];
505
+
506
+ const rows = this.stmtAllVectors.all();
507
+ const scored = [];
508
+
509
+ for (const row of rows) {
510
+ if (!row.vector) continue;
511
+ const buf = row.vector;
512
+ const docVector = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
513
+
514
+ let dot = 0;
515
+ let docNormSq = 0;
516
+ const dim = Math.min(qLen, docVector.length);
517
+
518
+ for (let i = 0; i < dim; i++) {
519
+ const qVal = queryVector[i];
520
+ const dVal = docVector[i];
521
+ dot += qVal * dVal;
522
+ docNormSq += dVal * dVal;
523
+ }
524
+
525
+ const docNorm = Math.sqrt(docNormSq);
526
+ const similarity = docNorm === 0 ? 0 : dot / (queryNorm * docNorm);
527
+
528
+ let parsedMeta = {};
529
+ if (row.metadata) {
530
+ try {
531
+ parsedMeta = JSON.parse(row.metadata);
532
+ } catch {
533
+ parsedMeta = {};
534
+ }
535
+ }
536
+
537
+ scored.push({
538
+ id: row.id,
539
+ text: row.text,
540
+ score: similarity,
541
+ metadata: parsedMeta,
542
+ });
543
+ }
544
+
545
+ // Sort descending by similarity score
546
+ scored.sort((a, b) => b.score - a.score);
547
+
548
+ return scored.slice(0, limit);
549
+ }
550
+
551
+ /**
552
+ * Delete documents and graph tables and reinitialize.
553
+ */
554
+ async deleteTable() {
555
+ if (!this.initialized) await this.init();
556
+ try {
557
+ this.db.exec(`
558
+ DROP TABLE IF EXISTS documents;
559
+ DROP TABLE IF EXISTS graph_nodes;
560
+ DROP TABLE IF EXISTS graph_edges;
561
+ `);
562
+ this.initialized = false;
563
+ await this.init();
564
+ } catch (_e) {
565
+ // Ignore if table does not exist
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Total number of indexed documents.
571
+ * @returns {number}
572
+ */
573
+ count() {
574
+ if (!this.initialized || !this.db) return 0;
575
+ const row = this.stmtCount.get();
576
+ return row ? Number(row.count) : 0;
577
+ }
578
+
579
+ /**
580
+ * Close the database connection.
581
+ */
582
+ close() {
583
+ if (this.db) {
584
+ try {
585
+ this.db.close();
586
+ } catch (_e) {
587
+ // ignore
588
+ }
589
+ this.db = null;
590
+ this.initialized = false;
591
+ }
592
+ }
593
+ }
594
+
595
+ export default SqliteVectorStore;