neuronlayer 0.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 (78) hide show
  1. package/CONTRIBUTING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/index.js +38016 -0
  5. package/esbuild.config.js +26 -0
  6. package/package.json +63 -0
  7. package/src/cli/commands.ts +382 -0
  8. package/src/core/adr-exporter.ts +253 -0
  9. package/src/core/architecture/architecture-enforcement.ts +228 -0
  10. package/src/core/architecture/duplicate-detector.ts +288 -0
  11. package/src/core/architecture/index.ts +6 -0
  12. package/src/core/architecture/pattern-learner.ts +306 -0
  13. package/src/core/architecture/pattern-library.ts +403 -0
  14. package/src/core/architecture/pattern-validator.ts +324 -0
  15. package/src/core/change-intelligence/bug-correlator.ts +444 -0
  16. package/src/core/change-intelligence/change-intelligence.ts +221 -0
  17. package/src/core/change-intelligence/change-tracker.ts +334 -0
  18. package/src/core/change-intelligence/fix-suggester.ts +340 -0
  19. package/src/core/change-intelligence/index.ts +5 -0
  20. package/src/core/code-verifier.ts +843 -0
  21. package/src/core/confidence/confidence-scorer.ts +251 -0
  22. package/src/core/confidence/conflict-checker.ts +289 -0
  23. package/src/core/confidence/index.ts +5 -0
  24. package/src/core/confidence/source-tracker.ts +263 -0
  25. package/src/core/confidence/warning-detector.ts +241 -0
  26. package/src/core/context-rot/compaction.ts +284 -0
  27. package/src/core/context-rot/context-health.ts +243 -0
  28. package/src/core/context-rot/context-rot-prevention.ts +213 -0
  29. package/src/core/context-rot/critical-context.ts +221 -0
  30. package/src/core/context-rot/drift-detector.ts +255 -0
  31. package/src/core/context-rot/index.ts +7 -0
  32. package/src/core/context.ts +263 -0
  33. package/src/core/decision-extractor.ts +339 -0
  34. package/src/core/decisions.ts +69 -0
  35. package/src/core/deja-vu.ts +421 -0
  36. package/src/core/engine.ts +1455 -0
  37. package/src/core/feature-context.ts +726 -0
  38. package/src/core/ghost-mode.ts +412 -0
  39. package/src/core/learning.ts +485 -0
  40. package/src/core/living-docs/activity-tracker.ts +296 -0
  41. package/src/core/living-docs/architecture-generator.ts +428 -0
  42. package/src/core/living-docs/changelog-generator.ts +348 -0
  43. package/src/core/living-docs/component-generator.ts +230 -0
  44. package/src/core/living-docs/doc-engine.ts +110 -0
  45. package/src/core/living-docs/doc-validator.ts +282 -0
  46. package/src/core/living-docs/index.ts +8 -0
  47. package/src/core/project-manager.ts +297 -0
  48. package/src/core/summarizer.ts +267 -0
  49. package/src/core/test-awareness/change-validator.ts +499 -0
  50. package/src/core/test-awareness/index.ts +5 -0
  51. package/src/index.ts +49 -0
  52. package/src/indexing/ast.ts +563 -0
  53. package/src/indexing/embeddings.ts +85 -0
  54. package/src/indexing/indexer.ts +245 -0
  55. package/src/indexing/watcher.ts +78 -0
  56. package/src/server/gateways/aggregator.ts +374 -0
  57. package/src/server/gateways/index.ts +473 -0
  58. package/src/server/gateways/memory-ghost.ts +343 -0
  59. package/src/server/gateways/memory-query.ts +452 -0
  60. package/src/server/gateways/memory-record.ts +346 -0
  61. package/src/server/gateways/memory-review.ts +410 -0
  62. package/src/server/gateways/memory-status.ts +517 -0
  63. package/src/server/gateways/memory-verify.ts +392 -0
  64. package/src/server/gateways/router.ts +434 -0
  65. package/src/server/gateways/types.ts +610 -0
  66. package/src/server/mcp.ts +154 -0
  67. package/src/server/resources.ts +85 -0
  68. package/src/server/tools.ts +2261 -0
  69. package/src/storage/database.ts +262 -0
  70. package/src/storage/tier1.ts +135 -0
  71. package/src/storage/tier2.ts +764 -0
  72. package/src/storage/tier3.ts +123 -0
  73. package/src/types/documentation.ts +619 -0
  74. package/src/types/index.ts +222 -0
  75. package/src/utils/config.ts +193 -0
  76. package/src/utils/files.ts +117 -0
  77. package/src/utils/time.ts +37 -0
  78. package/src/utils/tokens.ts +52 -0
@@ -0,0 +1,764 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { FileMetadata, Decision, SearchResult, DependencyRelation, CodeSymbol, Import, Export, SymbolKind } from '../types/index.js';
3
+
4
+ export class Tier2Storage {
5
+ private db: Database.Database;
6
+
7
+ constructor(db: Database.Database) {
8
+ this.db = db;
9
+ }
10
+
11
+ // File operations
12
+ upsertFile(
13
+ path: string,
14
+ contentHash: string,
15
+ preview: string,
16
+ language: string,
17
+ sizeBytes: number,
18
+ lineCount: number,
19
+ lastModified: number
20
+ ): number {
21
+ const stmt = this.db.prepare(`
22
+ INSERT INTO files (path, content_hash, preview, language, size_bytes, line_count, last_modified, indexed_at)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?, unixepoch())
24
+ ON CONFLICT(path) DO UPDATE SET
25
+ content_hash = excluded.content_hash,
26
+ preview = excluded.preview,
27
+ language = excluded.language,
28
+ size_bytes = excluded.size_bytes,
29
+ line_count = excluded.line_count,
30
+ last_modified = excluded.last_modified,
31
+ indexed_at = unixepoch()
32
+ RETURNING id
33
+ `);
34
+
35
+ const result = stmt.get(path, contentHash, preview, language, sizeBytes, lineCount, lastModified) as { id: number };
36
+ return result.id;
37
+ }
38
+
39
+ getFile(path: string): FileMetadata | null {
40
+ const stmt = this.db.prepare(`
41
+ SELECT id, path, content_hash as contentHash, preview, language,
42
+ size_bytes as sizeBytes, last_modified as lastModified, indexed_at as indexedAt
43
+ FROM files WHERE path = ?
44
+ `);
45
+ return stmt.get(path) as FileMetadata | null;
46
+ }
47
+
48
+ getFileById(id: number): FileMetadata | null {
49
+ const stmt = this.db.prepare(`
50
+ SELECT id, path, content_hash as contentHash, preview, language,
51
+ size_bytes as sizeBytes, last_modified as lastModified, indexed_at as indexedAt
52
+ FROM files WHERE id = ?
53
+ `);
54
+ return stmt.get(id) as FileMetadata | null;
55
+ }
56
+
57
+ deleteFile(path: string): void {
58
+ const stmt = this.db.prepare('DELETE FROM files WHERE path = ?');
59
+ stmt.run(path);
60
+ }
61
+
62
+ getAllFiles(): FileMetadata[] {
63
+ const stmt = this.db.prepare(`
64
+ SELECT id, path, content_hash as contentHash, preview, language,
65
+ size_bytes as sizeBytes, last_modified as lastModified, indexed_at as indexedAt
66
+ FROM files ORDER BY path
67
+ `);
68
+ return stmt.all() as FileMetadata[];
69
+ }
70
+
71
+ getFileCount(): number {
72
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM files');
73
+ const result = stmt.get() as { count: number };
74
+ return result.count;
75
+ }
76
+
77
+ getTotalLines(): number {
78
+ const stmt = this.db.prepare('SELECT COALESCE(SUM(line_count), 0) as total FROM files');
79
+ const result = stmt.get() as { total: number };
80
+ return result.total;
81
+ }
82
+
83
+ getLanguages(): string[] {
84
+ const stmt = this.db.prepare('SELECT DISTINCT language FROM files WHERE language IS NOT NULL ORDER BY language');
85
+ const results = stmt.all() as { language: string }[];
86
+ return results.map(r => r.language);
87
+ }
88
+
89
+ // Embedding operations
90
+ upsertEmbedding(fileId: number, embedding: Float32Array): void {
91
+ const buffer = Buffer.from(embedding.buffer);
92
+ const stmt = this.db.prepare(`
93
+ INSERT INTO embeddings (file_id, embedding, dimension)
94
+ VALUES (?, ?, ?)
95
+ ON CONFLICT(file_id) DO UPDATE SET
96
+ embedding = excluded.embedding,
97
+ dimension = excluded.dimension
98
+ `);
99
+ stmt.run(fileId, buffer, embedding.length);
100
+ }
101
+
102
+ getEmbedding(fileId: number): Float32Array | null {
103
+ const stmt = this.db.prepare('SELECT embedding, dimension FROM embeddings WHERE file_id = ?');
104
+ const result = stmt.get(fileId) as { embedding: Buffer; dimension: number } | undefined;
105
+
106
+ if (!result) return null;
107
+
108
+ return new Float32Array(result.embedding.buffer, result.embedding.byteOffset, result.dimension);
109
+ }
110
+
111
+ getAllEmbeddings(): Array<{ fileId: number; embedding: Float32Array }> {
112
+ const stmt = this.db.prepare('SELECT file_id, embedding, dimension FROM embeddings');
113
+ const results = stmt.all() as Array<{ file_id: number; embedding: Buffer; dimension: number }>;
114
+
115
+ return results.map(r => ({
116
+ fileId: r.file_id,
117
+ embedding: new Float32Array(r.embedding.buffer, r.embedding.byteOffset, r.dimension)
118
+ }));
119
+ }
120
+
121
+ // Search using cosine similarity (computed in JS since sqlite-vec may not be available)
122
+ search(queryEmbedding: Float32Array, limit: number = 10): SearchResult[] {
123
+ const allEmbeddings = this.getAllEmbeddings();
124
+ const results: Array<{ fileId: number; similarity: number }> = [];
125
+
126
+ for (const { fileId, embedding } of allEmbeddings) {
127
+ const similarity = this.cosineSimilarity(queryEmbedding, embedding);
128
+ results.push({ fileId, similarity });
129
+ }
130
+
131
+ // Sort by similarity descending
132
+ results.sort((a, b) => b.similarity - a.similarity);
133
+
134
+ // Get top results with file metadata
135
+ const topResults = results.slice(0, limit);
136
+ const searchResults: SearchResult[] = [];
137
+
138
+ for (const { fileId, similarity } of topResults) {
139
+ const file = this.getFileById(fileId);
140
+ if (file) {
141
+ searchResults.push({
142
+ file: file.path,
143
+ preview: file.preview,
144
+ similarity,
145
+ lineStart: 1,
146
+ lineEnd: 50, // Default, could be improved
147
+ lastModified: file.lastModified
148
+ });
149
+ }
150
+ }
151
+
152
+ return searchResults;
153
+ }
154
+
155
+ private cosineSimilarity(a: Float32Array, b: Float32Array): number {
156
+ if (a.length !== b.length) return 0;
157
+
158
+ let dotProduct = 0;
159
+ let normA = 0;
160
+ let normB = 0;
161
+
162
+ for (let i = 0; i < a.length; i++) {
163
+ dotProduct += a[i]! * b[i]!;
164
+ normA += a[i]! * a[i]!;
165
+ normB += b[i]! * b[i]!;
166
+ }
167
+
168
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
169
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
170
+ }
171
+
172
+ // Decision operations
173
+ upsertDecision(decision: Decision, embedding?: Float32Array): void {
174
+ const embeddingBuffer = embedding ? Buffer.from(embedding.buffer) : null;
175
+ const stmt = this.db.prepare(`
176
+ INSERT INTO decisions (id, title, description, files, tags, created_at, embedding, author, status, superseded_by)
177
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
178
+ ON CONFLICT(id) DO UPDATE SET
179
+ title = excluded.title,
180
+ description = excluded.description,
181
+ files = excluded.files,
182
+ tags = excluded.tags,
183
+ embedding = excluded.embedding,
184
+ author = excluded.author,
185
+ status = excluded.status,
186
+ superseded_by = excluded.superseded_by
187
+ `);
188
+ stmt.run(
189
+ decision.id,
190
+ decision.title,
191
+ decision.description,
192
+ JSON.stringify(decision.files),
193
+ JSON.stringify(decision.tags),
194
+ Math.floor(decision.createdAt.getTime() / 1000),
195
+ embeddingBuffer,
196
+ decision.author || null,
197
+ decision.status || 'accepted',
198
+ decision.supersededBy || null
199
+ );
200
+ }
201
+
202
+ getDecision(id: string): Decision | null {
203
+ const stmt = this.db.prepare('SELECT * FROM decisions WHERE id = ?');
204
+ const row = stmt.get(id) as {
205
+ id: string;
206
+ title: string;
207
+ description: string;
208
+ files: string;
209
+ tags: string;
210
+ created_at: number;
211
+ author: string | null;
212
+ status: string | null;
213
+ superseded_by: string | null;
214
+ } | undefined;
215
+
216
+ if (!row) return null;
217
+
218
+ return {
219
+ id: row.id,
220
+ title: row.title,
221
+ description: row.description,
222
+ files: JSON.parse(row.files || '[]'),
223
+ tags: JSON.parse(row.tags || '[]'),
224
+ createdAt: new Date(row.created_at * 1000),
225
+ author: row.author || undefined,
226
+ status: (row.status as Decision['status']) || undefined,
227
+ supersededBy: row.superseded_by || undefined
228
+ };
229
+ }
230
+
231
+ getRecentDecisions(limit: number = 10): Decision[] {
232
+ const stmt = this.db.prepare(`
233
+ SELECT id, title, description, files, tags, created_at, author, status, superseded_by
234
+ FROM decisions
235
+ ORDER BY created_at DESC
236
+ LIMIT ?
237
+ `);
238
+ const rows = stmt.all(limit) as Array<{
239
+ id: string;
240
+ title: string;
241
+ description: string;
242
+ files: string;
243
+ tags: string;
244
+ created_at: number;
245
+ author: string | null;
246
+ status: string | null;
247
+ superseded_by: string | null;
248
+ }>;
249
+
250
+ return rows.map(row => ({
251
+ id: row.id,
252
+ title: row.title,
253
+ description: row.description,
254
+ files: JSON.parse(row.files || '[]'),
255
+ tags: JSON.parse(row.tags || '[]'),
256
+ createdAt: new Date(row.created_at * 1000),
257
+ author: row.author || undefined,
258
+ status: (row.status as Decision['status']) || undefined,
259
+ supersededBy: row.superseded_by || undefined
260
+ }));
261
+ }
262
+
263
+ // Phase 4: Get all decisions (for export)
264
+ getAllDecisions(): Decision[] {
265
+ const stmt = this.db.prepare(`
266
+ SELECT id, title, description, files, tags, created_at, author, status, superseded_by
267
+ FROM decisions
268
+ ORDER BY created_at ASC
269
+ `);
270
+ const rows = stmt.all() as Array<{
271
+ id: string;
272
+ title: string;
273
+ description: string;
274
+ files: string;
275
+ tags: string;
276
+ created_at: number;
277
+ author: string | null;
278
+ status: string | null;
279
+ superseded_by: string | null;
280
+ }>;
281
+
282
+ return rows.map(row => ({
283
+ id: row.id,
284
+ title: row.title,
285
+ description: row.description,
286
+ files: JSON.parse(row.files || '[]'),
287
+ tags: JSON.parse(row.tags || '[]'),
288
+ createdAt: new Date(row.created_at * 1000),
289
+ author: row.author || undefined,
290
+ status: (row.status as Decision['status']) || undefined,
291
+ supersededBy: row.superseded_by || undefined
292
+ }));
293
+ }
294
+
295
+ // Phase 4: Update decision status
296
+ updateDecisionStatus(
297
+ decisionId: string,
298
+ status: 'proposed' | 'accepted' | 'deprecated' | 'superseded',
299
+ supersededBy?: string
300
+ ): boolean {
301
+ const stmt = this.db.prepare(`
302
+ UPDATE decisions
303
+ SET status = ?, superseded_by = ?
304
+ WHERE id = ?
305
+ `);
306
+ const result = stmt.run(status, supersededBy || null, decisionId);
307
+ return result.changes > 0;
308
+ }
309
+
310
+ searchDecisions(queryEmbedding: Float32Array, limit: number = 5): Decision[] {
311
+ // Get all decisions with embeddings
312
+ const stmt = this.db.prepare(`
313
+ SELECT id, title, description, files, tags, created_at, embedding, author, status, superseded_by
314
+ FROM decisions
315
+ WHERE embedding IS NOT NULL
316
+ `);
317
+ const rows = stmt.all() as Array<{
318
+ id: string;
319
+ title: string;
320
+ description: string;
321
+ files: string;
322
+ tags: string;
323
+ created_at: number;
324
+ embedding: Buffer;
325
+ author: string | null;
326
+ status: string | null;
327
+ superseded_by: string | null;
328
+ }>;
329
+
330
+ if (rows.length === 0) {
331
+ return this.getRecentDecisions(limit);
332
+ }
333
+
334
+ // Calculate similarity for each
335
+ const results = rows.map(row => {
336
+ const embedding = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, 384);
337
+ const similarity = this.cosineSimilarity(queryEmbedding, embedding);
338
+ return { row, similarity };
339
+ });
340
+
341
+ // Sort by similarity and return top results
342
+ results.sort((a, b) => b.similarity - a.similarity);
343
+
344
+ return results.slice(0, limit).map(({ row }) => ({
345
+ id: row.id,
346
+ title: row.title,
347
+ description: row.description,
348
+ files: JSON.parse(row.files || '[]'),
349
+ tags: JSON.parse(row.tags || '[]'),
350
+ createdAt: new Date(row.created_at * 1000),
351
+ author: row.author || undefined,
352
+ status: (row.status as Decision['status']) || undefined,
353
+ supersededBy: row.superseded_by || undefined
354
+ }));
355
+ }
356
+
357
+ // Dependency operations
358
+ addDependency(sourceFileId: number, targetFileId: number, relationship: string): void {
359
+ const stmt = this.db.prepare(`
360
+ INSERT OR IGNORE INTO dependencies (source_file_id, target_file_id, relationship)
361
+ VALUES (?, ?, ?)
362
+ `);
363
+ stmt.run(sourceFileId, targetFileId, relationship);
364
+ }
365
+
366
+ getDependencies(fileId: number): DependencyRelation[] {
367
+ const stmt = this.db.prepare(`
368
+ SELECT source_file_id as sourceFileId, target_file_id as targetFileId, relationship
369
+ FROM dependencies
370
+ WHERE source_file_id = ?
371
+ `);
372
+ return stmt.all(fileId) as DependencyRelation[];
373
+ }
374
+
375
+ getDependents(fileId: number): DependencyRelation[] {
376
+ const stmt = this.db.prepare(`
377
+ SELECT source_file_id as sourceFileId, target_file_id as targetFileId, relationship
378
+ FROM dependencies
379
+ WHERE target_file_id = ?
380
+ `);
381
+ return stmt.all(fileId) as DependencyRelation[];
382
+ }
383
+
384
+ clearDependencies(fileId: number): void {
385
+ const stmt = this.db.prepare('DELETE FROM dependencies WHERE source_file_id = ?');
386
+ stmt.run(fileId);
387
+ }
388
+
389
+ // Project summary
390
+ updateProjectSummary(
391
+ name: string,
392
+ description: string,
393
+ languages: string[],
394
+ keyDirectories: string[],
395
+ architectureNotes: string
396
+ ): void {
397
+ const stmt = this.db.prepare(`
398
+ INSERT INTO project_summary (id, name, description, languages, key_directories, architecture_notes, updated_at)
399
+ VALUES (1, ?, ?, ?, ?, ?, unixepoch())
400
+ ON CONFLICT(id) DO UPDATE SET
401
+ name = excluded.name,
402
+ description = excluded.description,
403
+ languages = excluded.languages,
404
+ key_directories = excluded.key_directories,
405
+ architecture_notes = excluded.architecture_notes,
406
+ updated_at = unixepoch()
407
+ `);
408
+ stmt.run(name, description, JSON.stringify(languages), JSON.stringify(keyDirectories), architectureNotes);
409
+ }
410
+
411
+ getProjectSummary(): { name: string; description: string; languages: string[]; keyDirectories: string[]; architectureNotes: string } | null {
412
+ const stmt = this.db.prepare('SELECT name, description, languages, key_directories, architecture_notes FROM project_summary WHERE id = 1');
413
+ const row = stmt.get() as {
414
+ name: string;
415
+ description: string;
416
+ languages: string;
417
+ key_directories: string;
418
+ architecture_notes: string;
419
+ } | undefined;
420
+
421
+ if (!row) return null;
422
+
423
+ return {
424
+ name: row.name,
425
+ description: row.description,
426
+ languages: JSON.parse(row.languages || '[]'),
427
+ keyDirectories: JSON.parse(row.key_directories || '[]'),
428
+ architectureNotes: row.architecture_notes
429
+ };
430
+ }
431
+
432
+ // Phase 2: Symbol operations
433
+
434
+ clearSymbols(fileId: number): void {
435
+ this.db.prepare('DELETE FROM symbols WHERE file_id = ?').run(fileId);
436
+ }
437
+
438
+ insertSymbol(symbol: CodeSymbol): number {
439
+ const stmt = this.db.prepare(`
440
+ INSERT INTO symbols (file_id, kind, name, signature, docstring, line_start, line_end, exported)
441
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
442
+ `);
443
+ const result = stmt.run(
444
+ symbol.fileId,
445
+ symbol.kind,
446
+ symbol.name,
447
+ symbol.signature || null,
448
+ symbol.docstring || null,
449
+ symbol.lineStart,
450
+ symbol.lineEnd,
451
+ symbol.exported ? 1 : 0
452
+ );
453
+ return Number(result.lastInsertRowid);
454
+ }
455
+
456
+ insertSymbols(symbols: CodeSymbol[]): void {
457
+ const stmt = this.db.prepare(`
458
+ INSERT INTO symbols (file_id, kind, name, signature, docstring, line_start, line_end, exported)
459
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
460
+ `);
461
+
462
+ const insertMany = this.db.transaction((syms: CodeSymbol[]) => {
463
+ for (const s of syms) {
464
+ stmt.run(s.fileId, s.kind, s.name, s.signature || null, s.docstring || null, s.lineStart, s.lineEnd, s.exported ? 1 : 0);
465
+ }
466
+ });
467
+
468
+ insertMany(symbols);
469
+ }
470
+
471
+ getSymbolsByFile(fileId: number): CodeSymbol[] {
472
+ const stmt = this.db.prepare(`
473
+ SELECT s.id, s.file_id as fileId, f.path as filePath, s.kind, s.name, s.signature, s.docstring,
474
+ s.line_start as lineStart, s.line_end as lineEnd, s.exported
475
+ FROM symbols s
476
+ JOIN files f ON s.file_id = f.id
477
+ WHERE s.file_id = ?
478
+ ORDER BY s.line_start
479
+ `);
480
+ const rows = stmt.all(fileId) as Array<{
481
+ id: number;
482
+ fileId: number;
483
+ filePath: string;
484
+ kind: string;
485
+ name: string;
486
+ signature: string | null;
487
+ docstring: string | null;
488
+ lineStart: number;
489
+ lineEnd: number;
490
+ exported: number;
491
+ }>;
492
+
493
+ return rows.map(r => ({
494
+ id: r.id,
495
+ fileId: r.fileId,
496
+ filePath: r.filePath,
497
+ kind: r.kind as SymbolKind,
498
+ name: r.name,
499
+ signature: r.signature || undefined,
500
+ docstring: r.docstring || undefined,
501
+ lineStart: r.lineStart,
502
+ lineEnd: r.lineEnd,
503
+ exported: r.exported === 1
504
+ }));
505
+ }
506
+
507
+ searchSymbols(name: string, kind?: SymbolKind, limit: number = 20): CodeSymbol[] {
508
+ let query = `
509
+ SELECT s.id, s.file_id as fileId, f.path as filePath, s.kind, s.name, s.signature, s.docstring,
510
+ s.line_start as lineStart, s.line_end as lineEnd, s.exported
511
+ FROM symbols s
512
+ JOIN files f ON s.file_id = f.id
513
+ WHERE s.name LIKE ?
514
+ `;
515
+
516
+ const params: (string | number)[] = [`%${name}%`];
517
+
518
+ if (kind) {
519
+ query += ' AND s.kind = ?';
520
+ params.push(kind);
521
+ }
522
+
523
+ query += ' ORDER BY CASE WHEN s.name = ? THEN 0 WHEN s.name LIKE ? THEN 1 ELSE 2 END, s.name LIMIT ?';
524
+ params.push(name, `${name}%`, limit);
525
+
526
+ const stmt = this.db.prepare(query);
527
+ const rows = stmt.all(...params) as Array<{
528
+ id: number;
529
+ fileId: number;
530
+ filePath: string;
531
+ kind: string;
532
+ name: string;
533
+ signature: string | null;
534
+ docstring: string | null;
535
+ lineStart: number;
536
+ lineEnd: number;
537
+ exported: number;
538
+ }>;
539
+
540
+ return rows.map(r => ({
541
+ id: r.id,
542
+ fileId: r.fileId,
543
+ filePath: r.filePath,
544
+ kind: r.kind as SymbolKind,
545
+ name: r.name,
546
+ signature: r.signature || undefined,
547
+ docstring: r.docstring || undefined,
548
+ lineStart: r.lineStart,
549
+ lineEnd: r.lineEnd,
550
+ exported: r.exported === 1
551
+ }));
552
+ }
553
+
554
+ getSymbolByName(name: string, kind?: SymbolKind): CodeSymbol | null {
555
+ let query = `
556
+ SELECT s.id, s.file_id as fileId, f.path as filePath, s.kind, s.name, s.signature, s.docstring,
557
+ s.line_start as lineStart, s.line_end as lineEnd, s.exported
558
+ FROM symbols s
559
+ JOIN files f ON s.file_id = f.id
560
+ WHERE s.name = ?
561
+ `;
562
+
563
+ const params: string[] = [name];
564
+
565
+ if (kind) {
566
+ query += ' AND s.kind = ?';
567
+ params.push(kind);
568
+ }
569
+
570
+ query += ' LIMIT 1';
571
+
572
+ const stmt = this.db.prepare(query);
573
+ const row = stmt.get(...params) as {
574
+ id: number;
575
+ fileId: number;
576
+ filePath: string;
577
+ kind: string;
578
+ name: string;
579
+ signature: string | null;
580
+ docstring: string | null;
581
+ lineStart: number;
582
+ lineEnd: number;
583
+ exported: number;
584
+ } | undefined;
585
+
586
+ if (!row) return null;
587
+
588
+ return {
589
+ id: row.id,
590
+ fileId: row.fileId,
591
+ filePath: row.filePath,
592
+ kind: row.kind as SymbolKind,
593
+ name: row.name,
594
+ signature: row.signature || undefined,
595
+ docstring: row.docstring || undefined,
596
+ lineStart: row.lineStart,
597
+ lineEnd: row.lineEnd,
598
+ exported: row.exported === 1
599
+ };
600
+ }
601
+
602
+ getSymbolCount(): number {
603
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM symbols');
604
+ const result = stmt.get() as { count: number };
605
+ return result.count;
606
+ }
607
+
608
+ // Phase 2: Import operations
609
+
610
+ clearImports(fileId: number): void {
611
+ this.db.prepare('DELETE FROM imports WHERE file_id = ?').run(fileId);
612
+ }
613
+
614
+ insertImports(imports: Import[]): void {
615
+ const stmt = this.db.prepare(`
616
+ INSERT INTO imports (file_id, imported_from, imported_symbols, is_default, is_namespace, line_number)
617
+ VALUES (?, ?, ?, ?, ?, ?)
618
+ `);
619
+
620
+ const insertMany = this.db.transaction((imps: Import[]) => {
621
+ for (const i of imps) {
622
+ stmt.run(
623
+ i.fileId,
624
+ i.importedFrom,
625
+ JSON.stringify(i.importedSymbols),
626
+ i.isDefault ? 1 : 0,
627
+ i.isNamespace ? 1 : 0,
628
+ i.lineNumber
629
+ );
630
+ }
631
+ });
632
+
633
+ insertMany(imports);
634
+ }
635
+
636
+ getImportsByFile(fileId: number): Import[] {
637
+ const stmt = this.db.prepare(`
638
+ SELECT i.file_id as fileId, f.path as filePath, i.imported_from as importedFrom,
639
+ i.imported_symbols as importedSymbols, i.is_default as isDefault,
640
+ i.is_namespace as isNamespace, i.line_number as lineNumber
641
+ FROM imports i
642
+ JOIN files f ON i.file_id = f.id
643
+ WHERE i.file_id = ?
644
+ `);
645
+ const rows = stmt.all(fileId) as Array<{
646
+ fileId: number;
647
+ filePath: string;
648
+ importedFrom: string;
649
+ importedSymbols: string;
650
+ isDefault: number;
651
+ isNamespace: number;
652
+ lineNumber: number;
653
+ }>;
654
+
655
+ return rows.map(r => ({
656
+ fileId: r.fileId,
657
+ filePath: r.filePath,
658
+ importedFrom: r.importedFrom,
659
+ importedSymbols: JSON.parse(r.importedSymbols),
660
+ isDefault: r.isDefault === 1,
661
+ isNamespace: r.isNamespace === 1,
662
+ lineNumber: r.lineNumber
663
+ }));
664
+ }
665
+
666
+ getFilesImporting(modulePath: string): Array<{ fileId: number; filePath: string }> {
667
+ const stmt = this.db.prepare(`
668
+ SELECT DISTINCT i.file_id as fileId, f.path as filePath
669
+ FROM imports i
670
+ JOIN files f ON i.file_id = f.id
671
+ WHERE i.imported_from LIKE ?
672
+ `);
673
+ return stmt.all(`%${modulePath}%`) as Array<{ fileId: number; filePath: string }>;
674
+ }
675
+
676
+ // Phase 2: Export operations
677
+
678
+ clearExports(fileId: number): void {
679
+ this.db.prepare('DELETE FROM exports WHERE file_id = ?').run(fileId);
680
+ }
681
+
682
+ insertExports(exports: Export[]): void {
683
+ const stmt = this.db.prepare(`
684
+ INSERT INTO exports (file_id, exported_name, local_name, is_default, line_number)
685
+ VALUES (?, ?, ?, ?, ?)
686
+ `);
687
+
688
+ const insertMany = this.db.transaction((exps: Export[]) => {
689
+ for (const e of exps) {
690
+ stmt.run(e.fileId, e.exportedName, e.localName || null, e.isDefault ? 1 : 0, e.lineNumber);
691
+ }
692
+ });
693
+
694
+ insertMany(exports);
695
+ }
696
+
697
+ getExportsByFile(fileId: number): Export[] {
698
+ const stmt = this.db.prepare(`
699
+ SELECT e.file_id as fileId, f.path as filePath, e.exported_name as exportedName,
700
+ e.local_name as localName, e.is_default as isDefault, e.line_number as lineNumber
701
+ FROM exports e
702
+ JOIN files f ON e.file_id = f.id
703
+ WHERE e.file_id = ?
704
+ `);
705
+ const rows = stmt.all(fileId) as Array<{
706
+ fileId: number;
707
+ filePath: string;
708
+ exportedName: string;
709
+ localName: string | null;
710
+ isDefault: number;
711
+ lineNumber: number;
712
+ }>;
713
+
714
+ return rows.map(r => ({
715
+ fileId: r.fileId,
716
+ filePath: r.filePath,
717
+ exportedName: r.exportedName,
718
+ localName: r.localName || undefined,
719
+ isDefault: r.isDefault === 1,
720
+ lineNumber: r.lineNumber
721
+ }));
722
+ }
723
+
724
+ // Phase 2: Dependency graph helpers
725
+
726
+ getFileDependencies(filePath: string): Array<{ file: string; imports: string[] }> {
727
+ const file = this.getFile(filePath);
728
+ if (!file) return [];
729
+
730
+ const imports = this.getImportsByFile(file.id);
731
+ const deps: Array<{ file: string; imports: string[] }> = [];
732
+
733
+ for (const imp of imports) {
734
+ deps.push({
735
+ file: imp.importedFrom,
736
+ imports: imp.importedSymbols
737
+ });
738
+ }
739
+
740
+ return deps;
741
+ }
742
+
743
+ getFileDependents(filePath: string): Array<{ file: string; imports: string[] }> {
744
+ // Find files that import this file
745
+ // This is a simplified version - in reality we'd need to resolve module paths
746
+ const fileName = filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') || '';
747
+ const importers = this.getFilesImporting(fileName);
748
+
749
+ const deps: Array<{ file: string; imports: string[] }> = [];
750
+
751
+ for (const importer of importers) {
752
+ const imports = this.getImportsByFile(importer.fileId);
753
+ const relevantImport = imports.find(i => i.importedFrom.includes(fileName));
754
+ if (relevantImport) {
755
+ deps.push({
756
+ file: importer.filePath,
757
+ imports: relevantImport.importedSymbols
758
+ });
759
+ }
760
+ }
761
+
762
+ return deps;
763
+ }
764
+ }