ai-mind-map 1.1.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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/change-tracker/change-log.d.ts +160 -0
  4. package/dist/change-tracker/change-log.d.ts.map +1 -0
  5. package/dist/change-tracker/change-log.js +507 -0
  6. package/dist/change-tracker/change-log.js.map +1 -0
  7. package/dist/change-tracker/diff-engine.d.ts +149 -0
  8. package/dist/change-tracker/diff-engine.d.ts.map +1 -0
  9. package/dist/change-tracker/diff-engine.js +530 -0
  10. package/dist/change-tracker/diff-engine.js.map +1 -0
  11. package/dist/change-tracker/watcher.d.ts +137 -0
  12. package/dist/change-tracker/watcher.d.ts.map +1 -0
  13. package/dist/change-tracker/watcher.js +300 -0
  14. package/dist/change-tracker/watcher.js.map +1 -0
  15. package/dist/cli.d.ts +20 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +937 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config.d.ts +38 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +222 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/context/compressor.d.ts +49 -0
  24. package/dist/context/compressor.d.ts.map +1 -0
  25. package/dist/context/compressor.js +769 -0
  26. package/dist/context/compressor.js.map +1 -0
  27. package/dist/context/progressive-disclosure.d.ts +71 -0
  28. package/dist/context/progressive-disclosure.d.ts.map +1 -0
  29. package/dist/context/progressive-disclosure.js +470 -0
  30. package/dist/context/progressive-disclosure.js.map +1 -0
  31. package/dist/context/token-budget.d.ts +121 -0
  32. package/dist/context/token-budget.d.ts.map +1 -0
  33. package/dist/context/token-budget.js +282 -0
  34. package/dist/context/token-budget.js.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +944 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/install.d.ts +66 -0
  40. package/dist/install.d.ts.map +1 -0
  41. package/dist/install.js +946 -0
  42. package/dist/install.js.map +1 -0
  43. package/dist/knowledge-graph/architecture.d.ts +213 -0
  44. package/dist/knowledge-graph/architecture.d.ts.map +1 -0
  45. package/dist/knowledge-graph/architecture.js +585 -0
  46. package/dist/knowledge-graph/architecture.js.map +1 -0
  47. package/dist/knowledge-graph/cypher.d.ts +113 -0
  48. package/dist/knowledge-graph/cypher.d.ts.map +1 -0
  49. package/dist/knowledge-graph/cypher.js +1051 -0
  50. package/dist/knowledge-graph/cypher.js.map +1 -0
  51. package/dist/knowledge-graph/dead-code.d.ts +121 -0
  52. package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
  53. package/dist/knowledge-graph/dead-code.js +331 -0
  54. package/dist/knowledge-graph/dead-code.js.map +1 -0
  55. package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
  56. package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
  57. package/dist/knowledge-graph/flow-analyzer.js +739 -0
  58. package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
  59. package/dist/knowledge-graph/graph.d.ts +291 -0
  60. package/dist/knowledge-graph/graph.d.ts.map +1 -0
  61. package/dist/knowledge-graph/graph.js +978 -0
  62. package/dist/knowledge-graph/graph.js.map +1 -0
  63. package/dist/knowledge-graph/index.d.ts +17 -0
  64. package/dist/knowledge-graph/index.d.ts.map +1 -0
  65. package/dist/knowledge-graph/index.js +14 -0
  66. package/dist/knowledge-graph/index.js.map +1 -0
  67. package/dist/knowledge-graph/indexer.d.ts +112 -0
  68. package/dist/knowledge-graph/indexer.d.ts.map +1 -0
  69. package/dist/knowledge-graph/indexer.js +506 -0
  70. package/dist/knowledge-graph/indexer.js.map +1 -0
  71. package/dist/knowledge-graph/pagerank.d.ts +141 -0
  72. package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
  73. package/dist/knowledge-graph/pagerank.js +493 -0
  74. package/dist/knowledge-graph/pagerank.js.map +1 -0
  75. package/dist/knowledge-graph/parser.d.ts +55 -0
  76. package/dist/knowledge-graph/parser.d.ts.map +1 -0
  77. package/dist/knowledge-graph/parser.js +1090 -0
  78. package/dist/knowledge-graph/parser.js.map +1 -0
  79. package/dist/knowledge-graph/snapshot.d.ts +107 -0
  80. package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
  81. package/dist/knowledge-graph/snapshot.js +435 -0
  82. package/dist/knowledge-graph/snapshot.js.map +1 -0
  83. package/dist/memory/decision-log.d.ts +151 -0
  84. package/dist/memory/decision-log.d.ts.map +1 -0
  85. package/dist/memory/decision-log.js +482 -0
  86. package/dist/memory/decision-log.js.map +1 -0
  87. package/dist/memory/persistent-memory.d.ts +182 -0
  88. package/dist/memory/persistent-memory.d.ts.map +1 -0
  89. package/dist/memory/persistent-memory.js +579 -0
  90. package/dist/memory/persistent-memory.js.map +1 -0
  91. package/dist/memory/session-memory.d.ts +165 -0
  92. package/dist/memory/session-memory.d.ts.map +1 -0
  93. package/dist/memory/session-memory.js +382 -0
  94. package/dist/memory/session-memory.js.map +1 -0
  95. package/dist/stress-test.d.ts +10 -0
  96. package/dist/stress-test.d.ts.map +1 -0
  97. package/dist/stress-test.js +258 -0
  98. package/dist/stress-test.js.map +1 -0
  99. package/dist/tools/advanced-tools.d.ts +32 -0
  100. package/dist/tools/advanced-tools.d.ts.map +1 -0
  101. package/dist/tools/advanced-tools.js +480 -0
  102. package/dist/tools/advanced-tools.js.map +1 -0
  103. package/dist/tools/change-tools.d.ts +76 -0
  104. package/dist/tools/change-tools.d.ts.map +1 -0
  105. package/dist/tools/change-tools.js +93 -0
  106. package/dist/tools/change-tools.js.map +1 -0
  107. package/dist/tools/context-tools.d.ts +68 -0
  108. package/dist/tools/context-tools.d.ts.map +1 -0
  109. package/dist/tools/context-tools.js +141 -0
  110. package/dist/tools/context-tools.js.map +1 -0
  111. package/dist/tools/debug-tools.d.ts +25 -0
  112. package/dist/tools/debug-tools.d.ts.map +1 -0
  113. package/dist/tools/debug-tools.js +286 -0
  114. package/dist/tools/debug-tools.js.map +1 -0
  115. package/dist/tools/evolving-tools.d.ts +23 -0
  116. package/dist/tools/evolving-tools.d.ts.map +1 -0
  117. package/dist/tools/evolving-tools.js +207 -0
  118. package/dist/tools/evolving-tools.js.map +1 -0
  119. package/dist/tools/flow-tools.d.ts +24 -0
  120. package/dist/tools/flow-tools.d.ts.map +1 -0
  121. package/dist/tools/flow-tools.js +265 -0
  122. package/dist/tools/flow-tools.js.map +1 -0
  123. package/dist/tools/graph-tools.d.ts +71 -0
  124. package/dist/tools/graph-tools.d.ts.map +1 -0
  125. package/dist/tools/graph-tools.js +165 -0
  126. package/dist/tools/graph-tools.js.map +1 -0
  127. package/dist/tools/memory-tools.d.ts +62 -0
  128. package/dist/tools/memory-tools.d.ts.map +1 -0
  129. package/dist/tools/memory-tools.js +195 -0
  130. package/dist/tools/memory-tools.js.map +1 -0
  131. package/dist/tools/smart-tools.d.ts +23 -0
  132. package/dist/tools/smart-tools.d.ts.map +1 -0
  133. package/dist/tools/smart-tools.js +482 -0
  134. package/dist/tools/smart-tools.js.map +1 -0
  135. package/dist/tools/snapshot-tools.d.ts +19 -0
  136. package/dist/tools/snapshot-tools.d.ts.map +1 -0
  137. package/dist/tools/snapshot-tools.js +149 -0
  138. package/dist/tools/snapshot-tools.js.map +1 -0
  139. package/dist/types.d.ts +181 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +45 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/utils/logger.d.ts +59 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +142 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/dist/utils/token-counter.d.ts +51 -0
  148. package/dist/utils/token-counter.d.ts.map +1 -0
  149. package/dist/utils/token-counter.js +181 -0
  150. package/dist/utils/token-counter.js.map +1 -0
  151. package/install.ps1 +321 -0
  152. package/install.sh +345 -0
  153. package/package.json +94 -0
  154. package/setup.bat +62 -0
@@ -0,0 +1,978 @@
1
+ /**
2
+ * AI Mind Map — SQLite-backed Knowledge Graph with FTS5
3
+ *
4
+ * Stores and queries the structural knowledge graph of a codebase.
5
+ * Provides CRUD operations, graph traversal, dependency chain tracing,
6
+ * full-text search, and statistics.
7
+ *
8
+ * Inspired by codebase-memory-mcp's Cypher-like queries.
9
+ */
10
+ import Database from 'better-sqlite3';
11
+ // ============================================================
12
+ // Schema SQL
13
+ // ============================================================
14
+ const SCHEMA_SQL = `
15
+ -- Core nodes table
16
+ CREATE TABLE IF NOT EXISTS nodes (
17
+ id TEXT PRIMARY KEY,
18
+ type TEXT NOT NULL,
19
+ name TEXT NOT NULL,
20
+ qualifiedName TEXT NOT NULL,
21
+ filePath TEXT NOT NULL,
22
+ startLine INTEGER NOT NULL,
23
+ endLine INTEGER NOT NULL,
24
+ signature TEXT NOT NULL DEFAULT '',
25
+ docComment TEXT,
26
+ hash TEXT NOT NULL,
27
+ language TEXT NOT NULL,
28
+ visibility TEXT NOT NULL DEFAULT 'unknown',
29
+ isAsync INTEGER NOT NULL DEFAULT 0,
30
+ isStatic INTEGER NOT NULL DEFAULT 0,
31
+ isExported INTEGER NOT NULL DEFAULT 0,
32
+ parameters TEXT,
33
+ returnType TEXT,
34
+ updatedAt INTEGER NOT NULL
35
+ );
36
+
37
+ -- Edges table with composite primary key
38
+ CREATE TABLE IF NOT EXISTS edges (
39
+ sourceId TEXT NOT NULL,
40
+ targetId TEXT NOT NULL,
41
+ type TEXT NOT NULL,
42
+ metadata TEXT,
43
+ PRIMARY KEY (sourceId, targetId, type)
44
+ );
45
+
46
+ -- Indexes for common queries
47
+ CREATE INDEX IF NOT EXISTS idx_nodes_filePath ON nodes(filePath);
48
+ CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
49
+ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
50
+ CREATE INDEX IF NOT EXISTS idx_nodes_language ON nodes(language);
51
+ CREATE INDEX IF NOT EXISTS idx_nodes_hash ON nodes(hash);
52
+ CREATE INDEX IF NOT EXISTS idx_edges_sourceId ON edges(sourceId);
53
+ CREATE INDEX IF NOT EXISTS idx_edges_targetId ON edges(targetId);
54
+ CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
55
+
56
+ -- FTS5 virtual table for full-text search across names, signatures, and doc comments
57
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
58
+ id UNINDEXED,
59
+ name,
60
+ qualifiedName,
61
+ signature,
62
+ docComment,
63
+ content='nodes',
64
+ content_rowid='rowid',
65
+ tokenize='porter unicode61'
66
+ );
67
+
68
+ -- Triggers to keep FTS in sync with nodes table
69
+ CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
70
+ INSERT INTO nodes_fts(rowid, id, name, qualifiedName, signature, docComment)
71
+ VALUES (new.rowid, new.id, new.name, new.qualifiedName, new.signature, new.docComment);
72
+ END;
73
+
74
+ CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
75
+ INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualifiedName, signature, docComment)
76
+ VALUES ('delete', old.rowid, old.id, old.name, old.qualifiedName, old.signature, old.docComment);
77
+ END;
78
+
79
+ CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
80
+ INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualifiedName, signature, docComment)
81
+ VALUES ('delete', old.rowid, old.id, old.name, old.qualifiedName, old.signature, old.docComment);
82
+ INSERT INTO nodes_fts(rowid, id, name, qualifiedName, signature, docComment)
83
+ VALUES (new.rowid, new.id, new.name, new.qualifiedName, new.signature, new.docComment);
84
+ END;
85
+
86
+ -- Metadata table for tracking schema version and stats
87
+ CREATE TABLE IF NOT EXISTS graph_meta (
88
+ key TEXT PRIMARY KEY,
89
+ value TEXT NOT NULL
90
+ );
91
+
92
+ -- Learned rules: AI-taught patterns that persist per-project
93
+ CREATE TABLE IF NOT EXISTS learned_rules (
94
+ id TEXT PRIMARY KEY,
95
+ type TEXT NOT NULL, -- 'classification' | 'search_alias' | 'code_pattern' | 'convention'
96
+ name TEXT NOT NULL,
97
+ description TEXT NOT NULL,
98
+ rule TEXT NOT NULL, -- JSON: the actual rule definition
99
+ created_at INTEGER NOT NULL,
100
+ updated_at INTEGER NOT NULL,
101
+ used_count INTEGER NOT NULL DEFAULT 0,
102
+ last_used_at INTEGER,
103
+ created_by TEXT NOT NULL DEFAULT 'ai'
104
+ );
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_learned_rules_type ON learned_rules(type);
107
+ CREATE INDEX IF NOT EXISTS idx_learned_rules_name ON learned_rules(name);
108
+ `;
109
+ const SCHEMA_VERSION = '2';
110
+ // ============================================================
111
+ // KnowledgeGraph Class
112
+ // ============================================================
113
+ /**
114
+ * SQLite-backed knowledge graph with FTS5 full-text search.
115
+ *
116
+ * All write operations use transactions for atomicity.
117
+ * Supports graph traversal, dependency analysis, and full-text search.
118
+ */
119
+ export class KnowledgeGraph {
120
+ db;
121
+ /**
122
+ * Create or open a knowledge graph database.
123
+ *
124
+ * @param dbPath - Path to the SQLite database file (or ':memory:' for in-memory)
125
+ */
126
+ constructor(dbPath) {
127
+ try {
128
+ this.db = new Database(dbPath);
129
+ this.db.pragma('journal_mode = WAL');
130
+ this.db.pragma('synchronous = NORMAL');
131
+ this.db.pragma('foreign_keys = ON');
132
+ this.db.pragma('cache_size = -64000');
133
+ this.initializeSchema();
134
+ }
135
+ catch (err) {
136
+ // If DB is corrupt, delete and retry once
137
+ try {
138
+ if (dbPath !== ':memory:') {
139
+ const fs = require('node:fs');
140
+ if (fs.existsSync(dbPath)) {
141
+ fs.unlinkSync(dbPath);
142
+ // Also remove WAL/SHM files
143
+ try {
144
+ fs.unlinkSync(dbPath + '-wal');
145
+ }
146
+ catch { }
147
+ try {
148
+ fs.unlinkSync(dbPath + '-shm');
149
+ }
150
+ catch { }
151
+ }
152
+ this.db = new Database(dbPath);
153
+ this.db.pragma('journal_mode = WAL');
154
+ this.db.pragma('synchronous = NORMAL');
155
+ this.db.pragma('foreign_keys = ON');
156
+ this.db.pragma('cache_size = -64000');
157
+ this.initializeSchema();
158
+ console.error('[ai-mind-map] Recovered from corrupt database — rebuilt from scratch');
159
+ }
160
+ else {
161
+ throw err;
162
+ }
163
+ }
164
+ catch {
165
+ throw new Error(`[ai-mind-map] Failed to initialize database at ${dbPath}: ${err}`);
166
+ }
167
+ }
168
+ }
169
+ /** Initialize or migrate the database schema */
170
+ initializeSchema() {
171
+ const currentVersion = this.getMeta('schema_version');
172
+ if (currentVersion === SCHEMA_VERSION) {
173
+ return; // Schema is current
174
+ }
175
+ if (currentVersion && currentVersion !== SCHEMA_VERSION) {
176
+ // In future versions, add migration logic here
177
+ // For now, drop and recreate
178
+ this.db.exec('DROP TABLE IF EXISTS nodes_fts');
179
+ this.db.exec('DROP TABLE IF EXISTS edges');
180
+ this.db.exec('DROP TABLE IF EXISTS nodes');
181
+ this.db.exec('DROP TABLE IF EXISTS graph_meta');
182
+ // Drop triggers
183
+ this.db.exec('DROP TRIGGER IF EXISTS nodes_ai');
184
+ this.db.exec('DROP TRIGGER IF EXISTS nodes_ad');
185
+ this.db.exec('DROP TRIGGER IF EXISTS nodes_au');
186
+ }
187
+ this.db.exec(SCHEMA_SQL);
188
+ this.setMeta('schema_version', SCHEMA_VERSION);
189
+ }
190
+ // ============================================================
191
+ // Metadata
192
+ // ============================================================
193
+ /** Get a metadata value by key */
194
+ getMeta(key) {
195
+ try {
196
+ const row = this.db.prepare('SELECT value FROM graph_meta WHERE key = ?').get(key);
197
+ return row?.value ?? null;
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ /** Set a metadata key-value pair */
204
+ setMeta(key, value) {
205
+ this.db.prepare('INSERT OR REPLACE INTO graph_meta (key, value) VALUES (?, ?)').run(key, value);
206
+ }
207
+ // ============================================================
208
+ // Node CRUD
209
+ // ============================================================
210
+ /** Serialize parameters array to JSON */
211
+ serializeParams(params) {
212
+ if (!params || params.length === 0)
213
+ return null;
214
+ return JSON.stringify(params);
215
+ }
216
+ /** Deserialize parameters JSON string to array */
217
+ deserializeParams(json) {
218
+ if (!json)
219
+ return undefined;
220
+ try {
221
+ return JSON.parse(json);
222
+ }
223
+ catch {
224
+ return undefined;
225
+ }
226
+ }
227
+ /** Convert a database row to a GraphNode */
228
+ rowToNode(row) {
229
+ return {
230
+ id: row.id,
231
+ type: row.type,
232
+ name: row.name,
233
+ qualifiedName: row.qualifiedName,
234
+ filePath: row.filePath,
235
+ startLine: row.startLine,
236
+ endLine: row.endLine,
237
+ signature: row.signature,
238
+ docComment: row.docComment ?? null,
239
+ hash: row.hash,
240
+ language: row.language,
241
+ visibility: row.visibility,
242
+ isAsync: Boolean(row.isAsync),
243
+ isStatic: Boolean(row.isStatic),
244
+ isExported: Boolean(row.isExported),
245
+ parameters: this.deserializeParams(row.parameters),
246
+ returnType: row.returnType ?? undefined,
247
+ updatedAt: row.updatedAt,
248
+ };
249
+ }
250
+ /**
251
+ * Insert or update a single node.
252
+ */
253
+ upsertNode(node) {
254
+ this.db.prepare(`
255
+ INSERT OR REPLACE INTO nodes (
256
+ id, type, name, qualifiedName, filePath, startLine, endLine,
257
+ signature, docComment, hash, language, visibility,
258
+ isAsync, isStatic, isExported, parameters, returnType, updatedAt
259
+ ) VALUES (
260
+ @id, @type, @name, @qualifiedName, @filePath, @startLine, @endLine,
261
+ @signature, @docComment, @hash, @language, @visibility,
262
+ @isAsync, @isStatic, @isExported, @parameters, @returnType, @updatedAt
263
+ )
264
+ `).run({
265
+ id: node.id,
266
+ type: node.type,
267
+ name: node.name,
268
+ qualifiedName: node.qualifiedName,
269
+ filePath: node.filePath,
270
+ startLine: node.startLine,
271
+ endLine: node.endLine,
272
+ signature: node.signature,
273
+ docComment: node.docComment,
274
+ hash: node.hash,
275
+ language: node.language,
276
+ visibility: node.visibility,
277
+ isAsync: node.isAsync ? 1 : 0,
278
+ isStatic: node.isStatic ? 1 : 0,
279
+ isExported: node.isExported ? 1 : 0,
280
+ parameters: this.serializeParams(node.parameters),
281
+ returnType: node.returnType ?? null,
282
+ updatedAt: node.updatedAt,
283
+ });
284
+ }
285
+ /**
286
+ * Bulk insert/update nodes within a transaction.
287
+ */
288
+ upsertNodes(nodes) {
289
+ if (nodes.length === 0)
290
+ return;
291
+ const insertStmt = this.db.prepare(`
292
+ INSERT OR REPLACE INTO nodes (
293
+ id, type, name, qualifiedName, filePath, startLine, endLine,
294
+ signature, docComment, hash, language, visibility,
295
+ isAsync, isStatic, isExported, parameters, returnType, updatedAt
296
+ ) VALUES (
297
+ @id, @type, @name, @qualifiedName, @filePath, @startLine, @endLine,
298
+ @signature, @docComment, @hash, @language, @visibility,
299
+ @isAsync, @isStatic, @isExported, @parameters, @returnType, @updatedAt
300
+ )
301
+ `);
302
+ const transaction = this.db.transaction((items) => {
303
+ for (const node of items) {
304
+ insertStmt.run({
305
+ id: node.id,
306
+ type: node.type,
307
+ name: node.name,
308
+ qualifiedName: node.qualifiedName,
309
+ filePath: node.filePath,
310
+ startLine: node.startLine,
311
+ endLine: node.endLine,
312
+ signature: node.signature,
313
+ docComment: node.docComment,
314
+ hash: node.hash,
315
+ language: node.language,
316
+ visibility: node.visibility,
317
+ isAsync: node.isAsync ? 1 : 0,
318
+ isStatic: node.isStatic ? 1 : 0,
319
+ isExported: node.isExported ? 1 : 0,
320
+ parameters: this.serializeParams(node.parameters),
321
+ returnType: node.returnType ?? null,
322
+ updatedAt: node.updatedAt,
323
+ });
324
+ }
325
+ });
326
+ transaction(nodes);
327
+ }
328
+ /**
329
+ * Get a node by its ID.
330
+ */
331
+ getNode(id) {
332
+ const row = this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
333
+ return row ? this.rowToNode(row) : null;
334
+ }
335
+ /**
336
+ * Get nodes by name (may return multiple matches across files).
337
+ */
338
+ getNodesByName(name) {
339
+ const rows = this.db.prepare('SELECT * FROM nodes WHERE name = ?').all(name);
340
+ return rows.map((r) => this.rowToNode(r));
341
+ }
342
+ /**
343
+ * Get all nodes of a specific type.
344
+ */
345
+ getNodesByType(type) {
346
+ const rows = this.db.prepare('SELECT * FROM nodes WHERE type = ?').all(type);
347
+ return rows.map((r) => this.rowToNode(r));
348
+ }
349
+ /**
350
+ * Delete a node and all its edges.
351
+ */
352
+ deleteNode(id) {
353
+ this.db.transaction(() => {
354
+ this.db.prepare('DELETE FROM edges WHERE sourceId = ? OR targetId = ?').run(id, id);
355
+ this.db.prepare('DELETE FROM nodes WHERE id = ?').run(id);
356
+ })();
357
+ }
358
+ /**
359
+ * Delete all nodes and edges for a specific file.
360
+ */
361
+ deleteFileNodes(filePath) {
362
+ let deletedCount = 0;
363
+ this.db.transaction(() => {
364
+ // Get all node IDs for this file
365
+ const nodeIds = this.db.prepare('SELECT id FROM nodes WHERE filePath = ?')
366
+ .all(filePath);
367
+ if (nodeIds.length === 0)
368
+ return;
369
+ deletedCount = nodeIds.length;
370
+ // Delete edges referencing these nodes
371
+ const placeholders = nodeIds.map(() => '?').join(',');
372
+ this.db.prepare(`DELETE FROM edges WHERE sourceId IN (${placeholders}) OR targetId IN (${placeholders})`).run(...nodeIds.map(n => n.id), ...nodeIds.map(n => n.id));
373
+ // Delete the nodes
374
+ this.db.prepare(`DELETE FROM nodes WHERE filePath = ?`).run(filePath);
375
+ })();
376
+ return deletedCount;
377
+ }
378
+ // ============================================================
379
+ // Edge CRUD
380
+ // ============================================================
381
+ /**
382
+ * Insert or update a single edge.
383
+ */
384
+ upsertEdge(edge) {
385
+ this.db.prepare(`
386
+ INSERT OR REPLACE INTO edges (sourceId, targetId, type, metadata)
387
+ VALUES (@sourceId, @targetId, @type, @metadata)
388
+ `).run({
389
+ sourceId: edge.sourceId,
390
+ targetId: edge.targetId,
391
+ type: edge.type,
392
+ metadata: edge.metadata ? JSON.stringify(edge.metadata) : null,
393
+ });
394
+ }
395
+ /**
396
+ * Bulk insert/update edges within a transaction.
397
+ */
398
+ upsertEdges(edges) {
399
+ if (edges.length === 0)
400
+ return;
401
+ const insertStmt = this.db.prepare(`
402
+ INSERT OR REPLACE INTO edges (sourceId, targetId, type, metadata)
403
+ VALUES (@sourceId, @targetId, @type, @metadata)
404
+ `);
405
+ const transaction = this.db.transaction((items) => {
406
+ for (const edge of items) {
407
+ insertStmt.run({
408
+ sourceId: edge.sourceId,
409
+ targetId: edge.targetId,
410
+ type: edge.type,
411
+ metadata: edge.metadata ? JSON.stringify(edge.metadata) : null,
412
+ });
413
+ }
414
+ });
415
+ transaction(edges);
416
+ }
417
+ /**
418
+ * Get all edges originating from a node.
419
+ */
420
+ getOutEdges(nodeId) {
421
+ const rows = this.db.prepare('SELECT * FROM edges WHERE sourceId = ?').all(nodeId);
422
+ return rows.map(r => ({
423
+ sourceId: r.sourceId,
424
+ targetId: r.targetId,
425
+ type: r.type,
426
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
427
+ }));
428
+ }
429
+ /**
430
+ * Get all edges pointing to a node.
431
+ */
432
+ getInEdges(nodeId) {
433
+ const rows = this.db.prepare('SELECT * FROM edges WHERE targetId = ?').all(nodeId);
434
+ return rows.map(r => ({
435
+ sourceId: r.sourceId,
436
+ targetId: r.targetId,
437
+ type: r.type,
438
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
439
+ }));
440
+ }
441
+ /**
442
+ * Get edges of a specific type originating from a node.
443
+ */
444
+ getOutEdgesByType(nodeId, type) {
445
+ const rows = this.db.prepare('SELECT * FROM edges WHERE sourceId = ? AND type = ?').all(nodeId, type);
446
+ return rows.map(r => ({
447
+ sourceId: r.sourceId,
448
+ targetId: r.targetId,
449
+ type: r.type,
450
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
451
+ }));
452
+ }
453
+ /**
454
+ * Get edges of a specific type pointing to a node.
455
+ */
456
+ getInEdgesByType(nodeId, type) {
457
+ const rows = this.db.prepare('SELECT * FROM edges WHERE targetId = ? AND type = ?').all(nodeId, type);
458
+ return rows.map(r => ({
459
+ sourceId: r.sourceId,
460
+ targetId: r.targetId,
461
+ type: r.type,
462
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
463
+ }));
464
+ }
465
+ /**
466
+ * Delete all edges for a specific file's nodes.
467
+ */
468
+ deleteFileEdges(filePath) {
469
+ const nodeIds = this.db.prepare('SELECT id FROM nodes WHERE filePath = ?')
470
+ .all(filePath);
471
+ if (nodeIds.length === 0)
472
+ return;
473
+ const placeholders = nodeIds.map(() => '?').join(',');
474
+ this.db.prepare(`DELETE FROM edges WHERE sourceId IN (${placeholders}) OR targetId IN (${placeholders})`).run(...nodeIds.map(n => n.id), ...nodeIds.map(n => n.id));
475
+ }
476
+ // ============================================================
477
+ // Graph Traversal
478
+ // ============================================================
479
+ /**
480
+ * Find all nodes that call a given node (callers / "who calls this?").
481
+ */
482
+ findCallers(nodeId) {
483
+ const rows = this.db.prepare(`
484
+ SELECT n.* FROM nodes n
485
+ JOIN edges e ON n.id = e.sourceId
486
+ WHERE e.targetId = ? AND e.type = 'calls'
487
+ `).all(nodeId);
488
+ return rows.map((r) => this.rowToNode(r));
489
+ }
490
+ /**
491
+ * Find all nodes that a given node calls (callees / "what does this call?").
492
+ */
493
+ findCallees(nodeId) {
494
+ const rows = this.db.prepare(`
495
+ SELECT n.* FROM nodes n
496
+ JOIN edges e ON n.id = e.targetId
497
+ WHERE e.sourceId = ? AND e.type = 'calls'
498
+ `).all(nodeId);
499
+ return rows.map((r) => this.rowToNode(r));
500
+ }
501
+ /**
502
+ * Find ancestors (parent classes, implemented interfaces) — traverse upward.
503
+ *
504
+ * @param nodeId - Starting node ID
505
+ * @param maxDepth - Maximum traversal depth (default 10)
506
+ */
507
+ findAncestors(nodeId, maxDepth = 10) {
508
+ const visited = new Set();
509
+ const result = [];
510
+ const queue = [{ id: nodeId, depth: 0 }];
511
+ while (queue.length > 0) {
512
+ const current = queue.shift();
513
+ if (current.depth >= maxDepth || visited.has(current.id))
514
+ continue;
515
+ visited.add(current.id);
516
+ const parentEdges = this.db.prepare(`
517
+ SELECT e.targetId FROM edges e
518
+ WHERE e.sourceId = ? AND e.type IN ('inherits', 'implements', 'contains')
519
+ `).all(current.id);
520
+ for (const { targetId } of parentEdges) {
521
+ if (!visited.has(targetId)) {
522
+ const node = this.getNode(targetId);
523
+ if (node) {
524
+ result.push(node);
525
+ queue.push({ id: targetId, depth: current.depth + 1 });
526
+ }
527
+ }
528
+ }
529
+ }
530
+ return result;
531
+ }
532
+ /**
533
+ * Find descendants (child classes, implementors, contained members) — traverse downward.
534
+ *
535
+ * @param nodeId - Starting node ID
536
+ * @param maxDepth - Maximum traversal depth (default 10)
537
+ */
538
+ findDescendants(nodeId, maxDepth = 10) {
539
+ const visited = new Set();
540
+ const result = [];
541
+ const queue = [{ id: nodeId, depth: 0 }];
542
+ while (queue.length > 0) {
543
+ const current = queue.shift();
544
+ if (current.depth >= maxDepth || visited.has(current.id))
545
+ continue;
546
+ visited.add(current.id);
547
+ const childEdges = this.db.prepare(`
548
+ SELECT e.sourceId FROM edges e
549
+ WHERE e.targetId = ? AND e.type IN ('inherits', 'implements', 'contains')
550
+ `).all(current.id);
551
+ for (const { sourceId } of childEdges) {
552
+ if (!visited.has(sourceId)) {
553
+ const node = this.getNode(sourceId);
554
+ if (node) {
555
+ result.push(node);
556
+ queue.push({ id: sourceId, depth: current.depth + 1 });
557
+ }
558
+ }
559
+ }
560
+ }
561
+ return result;
562
+ }
563
+ /**
564
+ * Blast radius analysis: find all nodes transitively affected if a node changes.
565
+ *
566
+ * Traces through calls, imports, inherits, implements, and uses edges.
567
+ *
568
+ * @param nodeId - The changed node
569
+ * @param maxDepth - Maximum traversal depth (default 5)
570
+ * @returns All nodes that depend on the changed node
571
+ */
572
+ blastRadius(nodeId, maxDepth = 5) {
573
+ const visited = new Set();
574
+ const result = [];
575
+ const dependencyTypes = ['calls', 'imports', 'inherits', 'implements', 'uses', 'depends_on'];
576
+ const typeFilter = dependencyTypes.map(t => `'${t}'`).join(',');
577
+ const queue = [{ id: nodeId, depth: 0 }];
578
+ while (queue.length > 0) {
579
+ const current = queue.shift();
580
+ if (current.depth >= maxDepth || visited.has(current.id))
581
+ continue;
582
+ visited.add(current.id);
583
+ // Find nodes that depend ON this node (reverse dependency)
584
+ const dependents = this.db.prepare(`
585
+ SELECT e.sourceId FROM edges e
586
+ WHERE e.targetId = ? AND e.type IN (${typeFilter})
587
+ `).all(current.id);
588
+ for (const { sourceId } of dependents) {
589
+ if (!visited.has(sourceId)) {
590
+ const node = this.getNode(sourceId);
591
+ if (node) {
592
+ result.push(node);
593
+ queue.push({ id: sourceId, depth: current.depth + 1 });
594
+ }
595
+ }
596
+ }
597
+ }
598
+ return result;
599
+ }
600
+ // ============================================================
601
+ // Full-Text Search
602
+ // ============================================================
603
+ /**
604
+ * Full-text search across node names, signatures, and doc comments.
605
+ *
606
+ * Uses FTS5 with porter stemming and unicode support, combined with
607
+ * LIKE search for exact substring matching. CamelCase/PascalCase queries
608
+ * are split into individual words for FTS5 matching.
609
+ *
610
+ * @param query - Search query
611
+ * @param limit - Maximum results (default 20)
612
+ * @returns Matching nodes sorted by relevance
613
+ */
614
+ search(query, limit = 20) {
615
+ const trimmed = query.trim();
616
+ if (!trimmed)
617
+ return [];
618
+ const likePattern = `%${trimmed}%`;
619
+ const sanitized = this.sanitizeFtsQuery(trimmed);
620
+ // Always run LIKE search. Combine with FTS via UNION when possible.
621
+ try {
622
+ const rows = this.db.prepare(`
623
+ SELECT * FROM (
624
+ SELECT n.*, CASE
625
+ WHEN n.name = @query THEN 0
626
+ WHEN n.name LIKE @like THEN 1
627
+ WHEN n.qualifiedName LIKE @like THEN 2
628
+ WHEN n.signature LIKE @like THEN 3
629
+ ELSE 4
630
+ END AS relevance
631
+ FROM nodes_fts fts
632
+ JOIN nodes n ON fts.id = n.id
633
+ WHERE nodes_fts MATCH @fts
634
+
635
+ UNION
636
+
637
+ SELECT n.*, CASE
638
+ WHEN n.name = @query THEN 0
639
+ WHEN n.name LIKE @like THEN 1
640
+ WHEN n.qualifiedName LIKE @like THEN 2
641
+ WHEN n.signature LIKE @like THEN 3
642
+ ELSE 4
643
+ END AS relevance
644
+ FROM nodes n
645
+ WHERE n.name LIKE @like
646
+ OR n.qualifiedName LIKE @like
647
+ OR n.signature LIKE @like
648
+ OR n.docComment LIKE @like
649
+ )
650
+ GROUP BY id
651
+ ORDER BY MIN(relevance), name
652
+ LIMIT @limit
653
+ `).all({ query: trimmed, like: likePattern, fts: sanitized, limit });
654
+ return rows.map((r) => this.rowToNode(r));
655
+ }
656
+ catch {
657
+ return this.searchLike(trimmed, limit);
658
+ }
659
+ }
660
+ /** Split camelCase/PascalCase into words and build an FTS5 AND query */
661
+ sanitizeFtsQuery(query) {
662
+ const stripped = query.replace(/[{}[\]()^~@!$]/g, ' ');
663
+ const words = [];
664
+ for (const token of stripped.split(/\s+/).filter(Boolean)) {
665
+ const parts = token
666
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
667
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
668
+ .replace(/[_\-./\\]/g, ' ')
669
+ .split(/\s+/)
670
+ .map(p => p.toLowerCase())
671
+ .filter(Boolean);
672
+ words.push(...parts);
673
+ }
674
+ if (words.length === 0)
675
+ return '""';
676
+ return words.map(w => `"${w}"`).join(' AND ');
677
+ }
678
+ /** Fallback LIKE-based search */
679
+ searchLike(query, limit) {
680
+ const pattern = `%${query}%`;
681
+ const rows = this.db.prepare(`
682
+ SELECT * FROM nodes
683
+ WHERE name LIKE ? OR qualifiedName LIKE ? OR signature LIKE ? OR docComment LIKE ?
684
+ ORDER BY
685
+ CASE
686
+ WHEN name = ? THEN 0
687
+ WHEN name LIKE ? THEN 1
688
+ WHEN qualifiedName LIKE ? THEN 2
689
+ WHEN signature LIKE ? THEN 3
690
+ ELSE 4
691
+ END,
692
+ name
693
+ LIMIT ?
694
+ `).all(pattern, pattern, pattern, pattern, query, pattern, pattern, pattern, limit);
695
+ return rows.map((r) => this.rowToNode(r));
696
+ }
697
+ // ============================================================
698
+ // File & Project Queries
699
+ // ============================================================
700
+ /**
701
+ * Get the structural overview of a single file (all nodes in that file).
702
+ */
703
+ getFileStructure(filePath) {
704
+ const rows = this.db.prepare('SELECT * FROM nodes WHERE filePath = ? ORDER BY startLine').all(filePath);
705
+ return rows.map((r) => this.rowToNode(r));
706
+ }
707
+ /**
708
+ * Get the content hash for a file node (used for change detection).
709
+ */
710
+ getFileHash(filePath) {
711
+ const row = this.db.prepare("SELECT hash FROM nodes WHERE filePath = ? AND type = 'file' LIMIT 1").get(filePath);
712
+ return row?.hash ?? null;
713
+ }
714
+ /**
715
+ * Get all indexed file paths.
716
+ */
717
+ getIndexedFiles() {
718
+ const rows = this.db.prepare("SELECT DISTINCT filePath FROM nodes WHERE type = 'file' ORDER BY filePath").all();
719
+ return rows.map(r => r.filePath);
720
+ }
721
+ /**
722
+ * Get the project overview: all files with their symbols.
723
+ *
724
+ * Returns a compact representation of the entire project structure,
725
+ * suitable for a repo map / table of contents.
726
+ *
727
+ * Includes nested members (class methods, interface members) — not
728
+ * just top-level symbols — so the AI actually knows what each class does.
729
+ *
730
+ * Uses a single SQL query instead of one-per-file (N+1 → 1).
731
+ */
732
+ getProjectOverview() {
733
+ const overview = new Map();
734
+ // Single query: get ALL non-file nodes, ordered by file then line
735
+ const allSymbols = this.db.prepare(`
736
+ SELECT * FROM nodes
737
+ WHERE type != 'file'
738
+ ORDER BY filePath, startLine
739
+ `).all();
740
+ for (const row of allSymbols) {
741
+ const node = this.rowToNode(row);
742
+ if (!overview.has(node.filePath)) {
743
+ overview.set(node.filePath, []);
744
+ }
745
+ overview.get(node.filePath).push(node);
746
+ }
747
+ return overview;
748
+ }
749
+ /**
750
+ * Get a compact string representation of a file's structure.
751
+ * Shows only signatures, not full code — key for token reduction.
752
+ */
753
+ getFileSignatures(filePath) {
754
+ const nodes = this.getFileStructure(filePath);
755
+ if (nodes.length === 0)
756
+ return '';
757
+ const lines = [`// ${filePath}`];
758
+ for (const node of nodes) {
759
+ if (node.type === 'file')
760
+ continue;
761
+ const indent = node.qualifiedName.includes('.') ? ' ' : '';
762
+ const prefix = node.docComment ? `${indent}/** ${node.docComment.split('\n')[0]} */\n` : '';
763
+ lines.push(`${prefix}${indent}${node.signature}`);
764
+ }
765
+ return lines.join('\n');
766
+ }
767
+ // ============================================================
768
+ // Statistics
769
+ // ============================================================
770
+ /**
771
+ * Get graph statistics.
772
+ */
773
+ getStats() {
774
+ const totalNodes = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get().count;
775
+ const totalEdges = this.db.prepare('SELECT COUNT(*) as count FROM edges').get().count;
776
+ const totalFiles = this.db.prepare("SELECT COUNT(*) as count FROM nodes WHERE type = 'file'").get().count;
777
+ const nodesByTypeRows = this.db.prepare('SELECT type, COUNT(*) as count FROM nodes GROUP BY type').all();
778
+ const nodesByType = {};
779
+ for (const { type, count } of nodesByTypeRows) {
780
+ nodesByType[type] = count;
781
+ }
782
+ const edgesByTypeRows = this.db.prepare('SELECT type, COUNT(*) as count FROM edges GROUP BY type').all();
783
+ const edgesByType = {};
784
+ for (const { type, count } of edgesByTypeRows) {
785
+ edgesByType[type] = count;
786
+ }
787
+ const langRows = this.db.prepare("SELECT language, COUNT(*) as count FROM nodes WHERE type = 'file' GROUP BY language").all();
788
+ const languageBreakdown = {};
789
+ for (const { language, count } of langRows) {
790
+ languageBreakdown[language] = count;
791
+ }
792
+ return { totalNodes, totalEdges, totalFiles, nodesByType, edgesByType, languageBreakdown };
793
+ }
794
+ // ============================================================
795
+ // Bulk Operations
796
+ // ============================================================
797
+ /**
798
+ * Replace all nodes and edges for a file atomically.
799
+ *
800
+ * Deletes existing nodes/edges for the file, then inserts new ones.
801
+ * This is the primary method used by the indexer during re-parsing.
802
+ */
803
+ replaceFileData(filePath, nodes, edges) {
804
+ this.db.transaction(() => {
805
+ this.deleteFileNodes(filePath);
806
+ this.upsertNodes(nodes);
807
+ this.upsertEdges(edges);
808
+ })();
809
+ }
810
+ /**
811
+ * Get all node IDs in the graph (used for PageRank).
812
+ */
813
+ getAllNodeIds() {
814
+ const rows = this.db.prepare('SELECT id FROM nodes').all();
815
+ return rows.map(r => r.id);
816
+ }
817
+ /**
818
+ * Get all edges in the graph (used for PageRank adjacency matrix).
819
+ */
820
+ getAllEdges() {
821
+ const rows = this.db.prepare('SELECT * FROM edges').all();
822
+ return rows.map(r => ({
823
+ sourceId: r.sourceId,
824
+ targetId: r.targetId,
825
+ type: r.type,
826
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
827
+ }));
828
+ }
829
+ /**
830
+ * Get nodes by a list of IDs (used by PageRank to return ranked results).
831
+ */
832
+ getNodesByIds(ids) {
833
+ if (ids.length === 0)
834
+ return [];
835
+ const placeholders = ids.map(() => '?').join(',');
836
+ const rows = this.db.prepare(`SELECT * FROM nodes WHERE id IN (${placeholders})`).all(...ids);
837
+ // Preserve the order of the input IDs
838
+ const nodeMap = new Map();
839
+ for (const row of rows) {
840
+ nodeMap.set(row.id, this.rowToNode(row));
841
+ }
842
+ return ids.map(id => nodeMap.get(id)).filter((n) => n !== undefined);
843
+ }
844
+ /**
845
+ * Clear all data from the graph.
846
+ */
847
+ clear() {
848
+ this.db.transaction(() => {
849
+ this.db.prepare('DELETE FROM edges').run();
850
+ this.db.prepare('DELETE FROM nodes').run();
851
+ })();
852
+ }
853
+ // ============================================================
854
+ // Learned Rules (Self-Evolving AI)
855
+ // ============================================================
856
+ /** Rule type for the learned_rules table */
857
+ static RULE_TYPES = ['classification', 'search_alias', 'code_pattern', 'convention'];
858
+ /**
859
+ * Teach the system a new rule. The rule persists in SQLite and is
860
+ * loaded automatically on future sessions.
861
+ */
862
+ addLearnedRule(rule) {
863
+ const id = `lr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
864
+ const now = Date.now();
865
+ // Check for duplicate by name + type
866
+ const existing = this.db.prepare('SELECT id FROM learned_rules WHERE name = ? AND type = ?').get(rule.name, rule.type);
867
+ if (existing) {
868
+ // Update existing rule
869
+ this.db.prepare('UPDATE learned_rules SET description = ?, rule = ?, updated_at = ? WHERE id = ?').run(rule.description, JSON.stringify(rule.rule), now, existing.id);
870
+ return { id: existing.id, created: false };
871
+ }
872
+ this.db.prepare(`
873
+ INSERT INTO learned_rules (id, type, name, description, rule, created_at, updated_at, created_by)
874
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
875
+ `).run(id, rule.type, rule.name, rule.description, JSON.stringify(rule.rule), now, now, rule.createdBy ?? 'ai');
876
+ return { id, created: true };
877
+ }
878
+ /**
879
+ * Get all learned rules, optionally filtered by type.
880
+ */
881
+ getLearnedRules(type) {
882
+ let rows;
883
+ if (type) {
884
+ rows = this.db.prepare('SELECT * FROM learned_rules WHERE type = ? ORDER BY used_count DESC').all(type);
885
+ }
886
+ else {
887
+ rows = this.db.prepare('SELECT * FROM learned_rules ORDER BY type, used_count DESC').all();
888
+ }
889
+ return rows.map((r) => ({
890
+ id: r.id,
891
+ type: r.type,
892
+ name: r.name,
893
+ description: r.description,
894
+ rule: JSON.parse(r.rule),
895
+ createdAt: r.created_at,
896
+ updatedAt: r.updated_at,
897
+ usedCount: r.used_count,
898
+ lastUsedAt: r.last_used_at,
899
+ createdBy: r.created_by,
900
+ }));
901
+ }
902
+ /**
903
+ * Increment usage counter for a learned rule (called when the rule is actually applied).
904
+ */
905
+ touchLearnedRule(id) {
906
+ this.db.prepare('UPDATE learned_rules SET used_count = used_count + 1, last_used_at = ? WHERE id = ?').run(Date.now(), id);
907
+ }
908
+ /**
909
+ * Delete a learned rule by ID or name.
910
+ */
911
+ deleteLearnedRule(idOrName) {
912
+ const result = this.db.prepare('DELETE FROM learned_rules WHERE id = ? OR name = ?').run(idOrName, idOrName);
913
+ return result.changes > 0;
914
+ }
915
+ /**
916
+ * Get classification rules learned by the AI.
917
+ * Returns them in a format ready to merge with CLASSIFICATION_SIGNALS.
918
+ */
919
+ getLearnedClassificationRules() {
920
+ const rules = this.getLearnedRules('classification');
921
+ return rules.map(r => ({
922
+ layer: r.rule.layer ?? 'unknown',
923
+ source: r.rule.source ?? 'path',
924
+ patterns: r.rule.patterns ?? [],
925
+ weight: r.rule.weight ?? 2,
926
+ name: r.name,
927
+ id: r.id,
928
+ }));
929
+ }
930
+ /**
931
+ * Get search aliases learned by the AI.
932
+ * When user searches for X, also search for Y, Z.
933
+ */
934
+ getLearnedSearchAliases() {
935
+ const rules = this.getLearnedRules('search_alias');
936
+ return rules.map(r => ({
937
+ term: r.rule.term ?? r.name,
938
+ aliases: r.rule.aliases ?? [],
939
+ id: r.id,
940
+ }));
941
+ }
942
+ /**
943
+ * Close the database connection.
944
+ */
945
+ close() {
946
+ this.db.close();
947
+ }
948
+ /** Check database health — verifies tables exist and are queryable */
949
+ isHealthy() {
950
+ try {
951
+ const nodes = this.db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
952
+ const edges = this.db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
953
+ const files = this.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE type = 'file'").get().c;
954
+ // Verify FTS is working
955
+ this.db.prepare("SELECT * FROM nodes_fts WHERE nodes_fts MATCH 'test' LIMIT 1").all();
956
+ return { healthy: true, stats: { nodes, edges, files } };
957
+ }
958
+ catch (err) {
959
+ return { healthy: false, error: err?.message ?? String(err) };
960
+ }
961
+ }
962
+ /**
963
+ * Check if the database is open.
964
+ */
965
+ get isOpen() {
966
+ return this.db.open;
967
+ }
968
+ /**
969
+ * Expose the underlying better-sqlite3 Database instance.
970
+ *
971
+ * Used by advanced query components (CypherEngine, DeadCodeDetector)
972
+ * that need to run raw SQL against the same database.
973
+ */
974
+ getDb() {
975
+ return this.db;
976
+ }
977
+ }
978
+ //# sourceMappingURL=graph.js.map