neuronlayer 0.1.9 → 0.2.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.

Potentially problematic release.


This version of neuronlayer might be problematic. Click here for more details.

Files changed (81) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +172 -90
  3. package/dist/index.js.map +7 -0
  4. package/package.json +6 -1
  5. package/esbuild.config.js +0 -26
  6. package/src/cli/commands.ts +0 -573
  7. package/src/core/adr-exporter.ts +0 -253
  8. package/src/core/architecture/architecture-enforcement.ts +0 -228
  9. package/src/core/architecture/duplicate-detector.ts +0 -288
  10. package/src/core/architecture/index.ts +0 -6
  11. package/src/core/architecture/pattern-learner.ts +0 -306
  12. package/src/core/architecture/pattern-library.ts +0 -403
  13. package/src/core/architecture/pattern-validator.ts +0 -324
  14. package/src/core/change-intelligence/bug-correlator.ts +0 -544
  15. package/src/core/change-intelligence/change-intelligence.ts +0 -264
  16. package/src/core/change-intelligence/change-tracker.ts +0 -334
  17. package/src/core/change-intelligence/fix-suggester.ts +0 -340
  18. package/src/core/change-intelligence/index.ts +0 -5
  19. package/src/core/code-verifier.ts +0 -843
  20. package/src/core/confidence/confidence-scorer.ts +0 -251
  21. package/src/core/confidence/conflict-checker.ts +0 -289
  22. package/src/core/confidence/index.ts +0 -5
  23. package/src/core/confidence/source-tracker.ts +0 -263
  24. package/src/core/confidence/warning-detector.ts +0 -241
  25. package/src/core/context-rot/compaction.ts +0 -284
  26. package/src/core/context-rot/context-health.ts +0 -243
  27. package/src/core/context-rot/context-rot-prevention.ts +0 -213
  28. package/src/core/context-rot/critical-context.ts +0 -221
  29. package/src/core/context-rot/drift-detector.ts +0 -255
  30. package/src/core/context-rot/index.ts +0 -7
  31. package/src/core/context.ts +0 -263
  32. package/src/core/decision-extractor.ts +0 -339
  33. package/src/core/decisions.ts +0 -69
  34. package/src/core/deja-vu.ts +0 -421
  35. package/src/core/engine.ts +0 -1646
  36. package/src/core/feature-context.ts +0 -726
  37. package/src/core/ghost-mode.ts +0 -465
  38. package/src/core/learning.ts +0 -519
  39. package/src/core/living-docs/activity-tracker.ts +0 -296
  40. package/src/core/living-docs/architecture-generator.ts +0 -428
  41. package/src/core/living-docs/changelog-generator.ts +0 -348
  42. package/src/core/living-docs/component-generator.ts +0 -230
  43. package/src/core/living-docs/doc-engine.ts +0 -134
  44. package/src/core/living-docs/doc-validator.ts +0 -282
  45. package/src/core/living-docs/index.ts +0 -8
  46. package/src/core/project-manager.ts +0 -301
  47. package/src/core/refresh/activity-gate.ts +0 -256
  48. package/src/core/refresh/git-staleness-checker.ts +0 -108
  49. package/src/core/refresh/index.ts +0 -27
  50. package/src/core/summarizer.ts +0 -290
  51. package/src/core/test-awareness/change-validator.ts +0 -499
  52. package/src/core/test-awareness/index.ts +0 -5
  53. package/src/index.ts +0 -90
  54. package/src/indexing/ast.ts +0 -868
  55. package/src/indexing/embeddings.ts +0 -85
  56. package/src/indexing/indexer.ts +0 -270
  57. package/src/indexing/watcher.ts +0 -78
  58. package/src/server/gateways/aggregator.ts +0 -374
  59. package/src/server/gateways/index.ts +0 -473
  60. package/src/server/gateways/memory-ghost.ts +0 -343
  61. package/src/server/gateways/memory-query.ts +0 -452
  62. package/src/server/gateways/memory-record.ts +0 -346
  63. package/src/server/gateways/memory-review.ts +0 -410
  64. package/src/server/gateways/memory-status.ts +0 -517
  65. package/src/server/gateways/memory-verify.ts +0 -392
  66. package/src/server/gateways/router.ts +0 -434
  67. package/src/server/gateways/types.ts +0 -610
  68. package/src/server/http.ts +0 -228
  69. package/src/server/mcp.ts +0 -154
  70. package/src/server/resources.ts +0 -85
  71. package/src/server/tools.ts +0 -2460
  72. package/src/storage/database.ts +0 -271
  73. package/src/storage/tier1.ts +0 -135
  74. package/src/storage/tier2.ts +0 -972
  75. package/src/storage/tier3.ts +0 -123
  76. package/src/types/documentation.ts +0 -619
  77. package/src/types/index.ts +0 -222
  78. package/src/utils/config.ts +0 -194
  79. package/src/utils/files.ts +0 -117
  80. package/src/utils/time.ts +0 -37
  81. package/src/utils/tokens.ts +0 -52
@@ -1,972 +0,0 @@
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
- // Use exact matching with common import path patterns to avoid false positives
668
- // e.g., "user" should not match "super-user-service"
669
- const stmt = this.db.prepare(`
670
- SELECT DISTINCT i.file_id as fileId, f.path as filePath
671
- FROM imports i
672
- JOIN files f ON i.file_id = f.id
673
- WHERE i.imported_from = ?
674
- OR i.imported_from LIKE ?
675
- OR i.imported_from LIKE ?
676
- OR i.imported_from LIKE ?
677
- `);
678
- return stmt.all(
679
- modulePath,
680
- `%/${modulePath}`, // ends with /modulePath
681
- `./${modulePath}`, // relative ./modulePath
682
- `../${modulePath}` // parent ../modulePath
683
- ) as Array<{ fileId: number; filePath: string }>;
684
- }
685
-
686
- // Phase 2: Export operations
687
-
688
- clearExports(fileId: number): void {
689
- this.db.prepare('DELETE FROM exports WHERE file_id = ?').run(fileId);
690
- }
691
-
692
- insertExports(exports: Export[]): void {
693
- const stmt = this.db.prepare(`
694
- INSERT INTO exports (file_id, exported_name, local_name, is_default, line_number)
695
- VALUES (?, ?, ?, ?, ?)
696
- `);
697
-
698
- const insertMany = this.db.transaction((exps: Export[]) => {
699
- for (const e of exps) {
700
- stmt.run(e.fileId, e.exportedName, e.localName || null, e.isDefault ? 1 : 0, e.lineNumber);
701
- }
702
- });
703
-
704
- insertMany(exports);
705
- }
706
-
707
- getExportsByFile(fileId: number): Export[] {
708
- const stmt = this.db.prepare(`
709
- SELECT e.file_id as fileId, f.path as filePath, e.exported_name as exportedName,
710
- e.local_name as localName, e.is_default as isDefault, e.line_number as lineNumber
711
- FROM exports e
712
- JOIN files f ON e.file_id = f.id
713
- WHERE e.file_id = ?
714
- `);
715
- const rows = stmt.all(fileId) as Array<{
716
- fileId: number;
717
- filePath: string;
718
- exportedName: string;
719
- localName: string | null;
720
- isDefault: number;
721
- lineNumber: number;
722
- }>;
723
-
724
- return rows.map(r => ({
725
- fileId: r.fileId,
726
- filePath: r.filePath,
727
- exportedName: r.exportedName,
728
- localName: r.localName || undefined,
729
- isDefault: r.isDefault === 1,
730
- lineNumber: r.lineNumber
731
- }));
732
- }
733
-
734
- // Phase 2: Dependency graph helpers
735
-
736
- getFileDependencies(filePath: string): Array<{ file: string; imports: string[] }> {
737
- const file = this.getFile(filePath);
738
- if (!file) return [];
739
-
740
- const imports = this.getImportsByFile(file.id);
741
- const deps: Array<{ file: string; imports: string[] }> = [];
742
-
743
- for (const imp of imports) {
744
- deps.push({
745
- file: imp.importedFrom,
746
- imports: imp.importedSymbols
747
- });
748
- }
749
-
750
- return deps;
751
- }
752
-
753
- getFileDependents(filePath: string): Array<{ file: string; imports: string[] }> {
754
- // Find files that import this file
755
- const fileName = filePath.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') || '';
756
- const importers = this.getFilesImporting(fileName);
757
-
758
- const deps: Array<{ file: string; imports: string[] }> = [];
759
-
760
- for (const importer of importers) {
761
- const imports = this.getImportsByFile(importer.fileId);
762
- const relevantImport = imports.find(i => {
763
- const importedName = i.importedFrom.split(/[/\\]/).pop()?.replace(/\.[^.]+$/, '') || '';
764
- return importedName === fileName || i.importedFrom.endsWith(`/${fileName}`) || i.importedFrom.endsWith(`./${fileName}`);
765
- });
766
- if (relevantImport) {
767
- deps.push({
768
- file: importer.filePath,
769
- imports: relevantImport.importedSymbols
770
- });
771
- }
772
- }
773
-
774
- return deps;
775
- }
776
-
777
- /**
778
- * Get ALL files affected by a change, walking the dependency graph.
779
- * depth=1 is direct importers only. depth=3 catches ripple effects.
780
- */
781
- getTransitiveDependents(
782
- filePath: string,
783
- maxDepth: number = 3
784
- ): Array<{ file: string; depth: number; imports: string[] }> {
785
- const visited = new Map<string, { depth: number; imports: string[] }>();
786
- const queue: Array<{ path: string; depth: number }> = [{ path: filePath, depth: 0 }];
787
-
788
- while (queue.length > 0) {
789
- const current = queue.shift()!;
790
- if (current.depth >= maxDepth) continue;
791
- if (visited.has(current.path) && visited.get(current.path)!.depth <= current.depth) continue;
792
-
793
- const dependents = this.getFileDependents(current.path);
794
- for (const dep of dependents) {
795
- const existingDepth = visited.get(dep.file)?.depth ?? Infinity;
796
- const newDepth = current.depth + 1;
797
-
798
- if (newDepth < existingDepth) {
799
- visited.set(dep.file, { depth: newDepth, imports: dep.imports });
800
- queue.push({ path: dep.file, depth: newDepth });
801
- }
802
- }
803
- }
804
-
805
- visited.delete(filePath); // don't include the original file
806
- return Array.from(visited.entries())
807
- .map(([file, info]) => ({ file, ...info }))
808
- .sort((a, b) => a.depth - b.depth);
809
- }
810
-
811
- /**
812
- * Get the full import graph as an adjacency list.
813
- * Returns { file → [files it imports] } for the whole project.
814
- */
815
- getFullDependencyGraph(): Map<string, string[]> {
816
- const stmt = this.db.prepare(`
817
- SELECT f.path as filePath, i.imported_from as importedFrom
818
- FROM imports i
819
- JOIN files f ON i.file_id = f.id
820
- `);
821
- const rows = stmt.all() as Array<{ filePath: string; importedFrom: string }>;
822
-
823
- const graph = new Map<string, string[]>();
824
- for (const row of rows) {
825
- if (!graph.has(row.filePath)) graph.set(row.filePath, []);
826
- graph.get(row.filePath)!.push(row.importedFrom);
827
- }
828
- return graph;
829
- }
830
-
831
- /**
832
- * Find circular dependencies in the project.
833
- * Returns arrays of file paths that form cycles.
834
- */
835
- findCircularDependencies(): Array<string[]> {
836
- const graph = this.getFullDependencyGraph();
837
- const cycles: Array<string[]> = [];
838
- const visited = new Set<string>();
839
- const stack = new Set<string>();
840
-
841
- const dfs = (node: string, path: string[]) => {
842
- if (stack.has(node)) {
843
- // Found cycle
844
- const cycleStart = path.indexOf(node);
845
- if (cycleStart >= 0) {
846
- cycles.push(path.slice(cycleStart).concat(node));
847
- }
848
- return;
849
- }
850
- if (visited.has(node)) return;
851
-
852
- visited.add(node);
853
- stack.add(node);
854
- path.push(node);
855
-
856
- const deps = graph.get(node) || [];
857
- for (const dep of deps) {
858
- // Resolve relative imports to file paths
859
- const resolved = this.resolveImportPath(node, dep);
860
- if (resolved) dfs(resolved, [...path]);
861
- }
862
-
863
- stack.delete(node);
864
- };
865
-
866
- for (const file of graph.keys()) {
867
- dfs(file, []);
868
- }
869
-
870
- // Deduplicate cycles (same cycle can be found from different starting points)
871
- const uniqueCycles: Array<string[]> = [];
872
- const seen = new Set<string>();
873
- for (const cycle of cycles) {
874
- const normalized = [...cycle].sort().join('|');
875
- if (!seen.has(normalized)) {
876
- seen.add(normalized);
877
- uniqueCycles.push(cycle);
878
- }
879
- }
880
-
881
- return uniqueCycles;
882
- }
883
-
884
- /**
885
- * Resolve a relative import path to an actual file path in the database.
886
- */
887
- resolveImportPath(fromFile: string, importPath: string): string | null {
888
- // Skip external packages (node_modules)
889
- if (!importPath.startsWith('.') && !importPath.startsWith('/')) return null;
890
-
891
- const dir = fromFile.split(/[/\\]/).slice(0, -1).join('/');
892
-
893
- // Normalize the import path
894
- let resolved = importPath;
895
- if (importPath.startsWith('./')) {
896
- resolved = dir + '/' + importPath.slice(2);
897
- } else if (importPath.startsWith('../')) {
898
- const parts = dir.split('/');
899
- let impParts = importPath.split('/');
900
- while (impParts[0] === '..') {
901
- parts.pop();
902
- impParts.shift();
903
- }
904
- resolved = parts.join('/') + '/' + impParts.join('/');
905
- }
906
-
907
- // Remove extension if present
908
- const baseName = resolved.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
909
-
910
- // Try to find a matching file in the database
911
- const stmt = this.db.prepare(`
912
- SELECT path FROM files
913
- WHERE path = ? OR path = ? OR path = ? OR path = ?
914
- OR path = ? OR path = ?
915
- LIMIT 1
916
- `);
917
- const result = stmt.get(
918
- `${baseName}.ts`, `${baseName}.tsx`,
919
- `${baseName}.js`, `${baseName}.jsx`,
920
- `${baseName}/index.ts`, `${baseName}/index.js`
921
- ) as { path: string } | undefined;
922
-
923
- return result?.path || null;
924
- }
925
-
926
- /**
927
- * Resolve an import path to a file record in the database.
928
- * Used by indexer to build the dependencies table.
929
- */
930
- resolveImportToFile(
931
- sourceFilePath: string,
932
- importPath: string
933
- ): { id: number; path: string } | null {
934
- // Skip external packages
935
- if (!importPath.startsWith('.') && !importPath.startsWith('/')) return null;
936
-
937
- const sourceDir = sourceFilePath.split(/[/\\]/).slice(0, -1).join('/');
938
-
939
- // Normalize the import path
940
- let resolved = importPath;
941
- if (importPath.startsWith('./')) {
942
- resolved = sourceDir + '/' + importPath.slice(2);
943
- } else if (importPath.startsWith('../')) {
944
- const parts = sourceDir.split('/');
945
- let impParts = importPath.split('/');
946
- while (impParts[0] === '..') {
947
- parts.pop();
948
- impParts.shift();
949
- }
950
- resolved = parts.join('/') + '/' + impParts.join('/');
951
- }
952
-
953
- // Remove extension if present
954
- resolved = resolved.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
955
-
956
- // Try exact matches with common extensions
957
- const stmt = this.db.prepare(`
958
- SELECT id, path FROM files
959
- WHERE path = ? OR path = ? OR path = ? OR path = ?
960
- OR path = ? OR path = ?
961
- LIMIT 1
962
- `);
963
-
964
- const result = stmt.get(
965
- `${resolved}.ts`, `${resolved}.tsx`,
966
- `${resolved}.js`, `${resolved}.jsx`,
967
- `${resolved}/index.ts`, `${resolved}/index.js`
968
- ) as { id: number; path: string } | undefined;
969
-
970
- return result || null;
971
- }
972
- }