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.
- package/CONTRIBUTING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/index.js +38016 -0
- package/esbuild.config.js +26 -0
- package/package.json +63 -0
- package/src/cli/commands.ts +382 -0
- package/src/core/adr-exporter.ts +253 -0
- package/src/core/architecture/architecture-enforcement.ts +228 -0
- package/src/core/architecture/duplicate-detector.ts +288 -0
- package/src/core/architecture/index.ts +6 -0
- package/src/core/architecture/pattern-learner.ts +306 -0
- package/src/core/architecture/pattern-library.ts +403 -0
- package/src/core/architecture/pattern-validator.ts +324 -0
- package/src/core/change-intelligence/bug-correlator.ts +444 -0
- package/src/core/change-intelligence/change-intelligence.ts +221 -0
- package/src/core/change-intelligence/change-tracker.ts +334 -0
- package/src/core/change-intelligence/fix-suggester.ts +340 -0
- package/src/core/change-intelligence/index.ts +5 -0
- package/src/core/code-verifier.ts +843 -0
- package/src/core/confidence/confidence-scorer.ts +251 -0
- package/src/core/confidence/conflict-checker.ts +289 -0
- package/src/core/confidence/index.ts +5 -0
- package/src/core/confidence/source-tracker.ts +263 -0
- package/src/core/confidence/warning-detector.ts +241 -0
- package/src/core/context-rot/compaction.ts +284 -0
- package/src/core/context-rot/context-health.ts +243 -0
- package/src/core/context-rot/context-rot-prevention.ts +213 -0
- package/src/core/context-rot/critical-context.ts +221 -0
- package/src/core/context-rot/drift-detector.ts +255 -0
- package/src/core/context-rot/index.ts +7 -0
- package/src/core/context.ts +263 -0
- package/src/core/decision-extractor.ts +339 -0
- package/src/core/decisions.ts +69 -0
- package/src/core/deja-vu.ts +421 -0
- package/src/core/engine.ts +1455 -0
- package/src/core/feature-context.ts +726 -0
- package/src/core/ghost-mode.ts +412 -0
- package/src/core/learning.ts +485 -0
- package/src/core/living-docs/activity-tracker.ts +296 -0
- package/src/core/living-docs/architecture-generator.ts +428 -0
- package/src/core/living-docs/changelog-generator.ts +348 -0
- package/src/core/living-docs/component-generator.ts +230 -0
- package/src/core/living-docs/doc-engine.ts +110 -0
- package/src/core/living-docs/doc-validator.ts +282 -0
- package/src/core/living-docs/index.ts +8 -0
- package/src/core/project-manager.ts +297 -0
- package/src/core/summarizer.ts +267 -0
- package/src/core/test-awareness/change-validator.ts +499 -0
- package/src/core/test-awareness/index.ts +5 -0
- package/src/index.ts +49 -0
- package/src/indexing/ast.ts +563 -0
- package/src/indexing/embeddings.ts +85 -0
- package/src/indexing/indexer.ts +245 -0
- package/src/indexing/watcher.ts +78 -0
- package/src/server/gateways/aggregator.ts +374 -0
- package/src/server/gateways/index.ts +473 -0
- package/src/server/gateways/memory-ghost.ts +343 -0
- package/src/server/gateways/memory-query.ts +452 -0
- package/src/server/gateways/memory-record.ts +346 -0
- package/src/server/gateways/memory-review.ts +410 -0
- package/src/server/gateways/memory-status.ts +517 -0
- package/src/server/gateways/memory-verify.ts +392 -0
- package/src/server/gateways/router.ts +434 -0
- package/src/server/gateways/types.ts +610 -0
- package/src/server/mcp.ts +154 -0
- package/src/server/resources.ts +85 -0
- package/src/server/tools.ts +2261 -0
- package/src/storage/database.ts +262 -0
- package/src/storage/tier1.ts +135 -0
- package/src/storage/tier2.ts +764 -0
- package/src/storage/tier3.ts +123 -0
- package/src/types/documentation.ts +619 -0
- package/src/types/index.ts +222 -0
- package/src/utils/config.ts +193 -0
- package/src/utils/files.ts +117 -0
- package/src/utils/time.ts +37 -0
- 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
|
+
}
|