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,421 @@
1
+ /**
2
+ * Déjà Vu Detection - Similar Problem Recognition
3
+ *
4
+ * Detects when a query or code pattern is similar to something solved before.
5
+ * Surfaces past solutions to prevent reinventing the wheel.
6
+ * "You solved a similar problem 2 weeks ago in auth.ts"
7
+ */
8
+
9
+ import type Database from 'better-sqlite3';
10
+ import type { EmbeddingGenerator } from '../indexing/embeddings.js';
11
+ import type { Tier2Storage } from '../storage/tier2.js';
12
+ import { createHash } from 'crypto';
13
+ import { formatTimeAgoWithContext } from '../utils/time.js';
14
+
15
+ export interface DejaVuMatch {
16
+ type: 'query' | 'solution' | 'fix' | 'pattern';
17
+ similarity: number;
18
+ when: Date;
19
+ file: string;
20
+ snippet: string;
21
+ message: string;
22
+ context?: string;
23
+ }
24
+
25
+ export interface PastQuery {
26
+ queryHash: string;
27
+ queryText: string;
28
+ resultFiles: string[];
29
+ timestamp: Date;
30
+ wasUseful: boolean;
31
+ usefulness: number;
32
+ }
33
+
34
+ export interface PastSolution {
35
+ file: string;
36
+ snippet: string;
37
+ query: string;
38
+ timestamp: Date;
39
+ similarity: number;
40
+ }
41
+
42
+ interface QueryPatternRow {
43
+ id: number;
44
+ query_hash: string;
45
+ query_text: string;
46
+ result_files: string;
47
+ hit_count: number;
48
+ avg_usefulness: number;
49
+ last_used: number;
50
+ }
51
+
52
+ interface UsageEventRow {
53
+ id: number;
54
+ event_type: string;
55
+ file_path: string | null;
56
+ query: string | null;
57
+ context_used: number;
58
+ timestamp: number;
59
+ }
60
+
61
+ export class DejaVuDetector {
62
+ private db: Database.Database;
63
+ private tier2: Tier2Storage;
64
+ private embeddingGenerator: EmbeddingGenerator;
65
+
66
+ // Thresholds
67
+ private readonly SIMILARITY_THRESHOLD = 0.7;
68
+ private readonly MIN_USEFULNESS = 0.3;
69
+ private readonly MAX_AGE_DAYS = 90;
70
+
71
+ constructor(
72
+ db: Database.Database,
73
+ tier2: Tier2Storage,
74
+ embeddingGenerator: EmbeddingGenerator
75
+ ) {
76
+ this.db = db;
77
+ this.tier2 = tier2;
78
+ this.embeddingGenerator = embeddingGenerator;
79
+ }
80
+
81
+ /**
82
+ * Find similar past problems, solutions, and fixes
83
+ */
84
+ async findSimilar(query: string, limit: number = 3): Promise<DejaVuMatch[]> {
85
+ const matches: DejaVuMatch[] = [];
86
+
87
+ // Run searches in parallel
88
+ const [pastQueries, pastSolutions, pastFixes] = await Promise.all([
89
+ this.searchPastQueries(query),
90
+ this.searchPastSolutions(query),
91
+ this.searchPastFixes(query),
92
+ ]);
93
+
94
+ matches.push(...pastQueries, ...pastSolutions, ...pastFixes);
95
+
96
+ // Sort by similarity and recency
97
+ return matches
98
+ .sort((a, b) => {
99
+ // Weight similarity higher, but give bonus to more recent
100
+ const recencyA = this.getRecencyScore(a.when);
101
+ const recencyB = this.getRecencyScore(b.when);
102
+ const scoreA = a.similarity * 0.7 + recencyA * 0.3;
103
+ const scoreB = b.similarity * 0.7 + recencyB * 0.3;
104
+ return scoreB - scoreA;
105
+ })
106
+ .slice(0, limit);
107
+ }
108
+
109
+ /**
110
+ * Search for similar past queries with high usefulness
111
+ */
112
+ async searchPastQueries(query: string): Promise<DejaVuMatch[]> {
113
+ const matches: DejaVuMatch[] = [];
114
+
115
+ try {
116
+ // Get query embedding
117
+ const embedding = await this.embeddingGenerator.embed(query);
118
+
119
+ // Get recent useful queries
120
+ const stmt = this.db.prepare(`
121
+ SELECT query_hash, query_text, result_files, avg_usefulness, last_used
122
+ FROM query_patterns
123
+ WHERE avg_usefulness >= ?
124
+ AND last_used > unixepoch() - ? * 86400
125
+ ORDER BY avg_usefulness DESC, last_used DESC
126
+ LIMIT 50
127
+ `);
128
+
129
+ const rows = stmt.all(this.MIN_USEFULNESS, this.MAX_AGE_DAYS) as QueryPatternRow[];
130
+
131
+ for (const row of rows) {
132
+ // Calculate text similarity
133
+ const similarity = this.calculateTextSimilarity(query, row.query_text);
134
+
135
+ if (similarity >= this.SIMILARITY_THRESHOLD) {
136
+ const resultFiles = this.parseJsonArray(row.result_files);
137
+ const primaryFile = resultFiles[0] || 'unknown';
138
+
139
+ matches.push({
140
+ type: 'query',
141
+ similarity,
142
+ when: new Date(row.last_used * 1000),
143
+ file: primaryFile,
144
+ snippet: row.query_text.slice(0, 100),
145
+ message: formatTimeAgoWithContext(new Date(row.last_used * 1000), 'asked a similar question', primaryFile),
146
+ context: resultFiles.length > 1 ? `Also involved: ${resultFiles.slice(1, 4).join(', ')}` : undefined,
147
+ });
148
+ }
149
+ }
150
+ } catch (error) {
151
+ console.error('Error searching past queries:', error);
152
+ }
153
+
154
+ return matches;
155
+ }
156
+
157
+ /**
158
+ * Search for past solutions to similar problems
159
+ */
160
+ async searchPastSolutions(query: string): Promise<DejaVuMatch[]> {
161
+ const matches: DejaVuMatch[] = [];
162
+
163
+ try {
164
+ // Search codebase for semantically similar content
165
+ const embedding = await this.embeddingGenerator.embed(query);
166
+ const searchResults = this.tier2.search(embedding, 10);
167
+
168
+ // Find results that were previously part of useful context
169
+ for (const result of searchResults) {
170
+ if (result.similarity < this.SIMILARITY_THRESHOLD) continue;
171
+
172
+ // Check if this file was used in a useful query before
173
+ const usageStmt = this.db.prepare(`
174
+ SELECT ue.query, ue.timestamp, qp.avg_usefulness
175
+ FROM usage_events ue
176
+ LEFT JOIN query_patterns qp ON ue.query = qp.query_text
177
+ WHERE ue.file_path = ?
178
+ AND ue.event_type = 'context_used'
179
+ AND ue.timestamp > unixepoch() - ? * 86400
180
+ AND (qp.avg_usefulness IS NULL OR qp.avg_usefulness >= ?)
181
+ ORDER BY ue.timestamp DESC
182
+ LIMIT 1
183
+ `);
184
+
185
+ const usage = usageStmt.get(result.file, this.MAX_AGE_DAYS, this.MIN_USEFULNESS) as {
186
+ query: string;
187
+ timestamp: number;
188
+ avg_usefulness: number | null;
189
+ } | undefined;
190
+
191
+ if (usage) {
192
+ matches.push({
193
+ type: 'solution',
194
+ similarity: result.similarity,
195
+ when: new Date(usage.timestamp * 1000),
196
+ file: result.file,
197
+ snippet: result.preview.slice(0, 150),
198
+ message: formatTimeAgoWithContext(new Date(usage.timestamp * 1000), 'worked on this', result.file),
199
+ context: usage.query ? `For: "${usage.query.slice(0, 50)}..."` : undefined,
200
+ });
201
+ }
202
+ }
203
+ } catch (error) {
204
+ console.error('Error searching past solutions:', error);
205
+ }
206
+
207
+ return matches;
208
+ }
209
+
210
+ /**
211
+ * Search for past bug fixes with similar error patterns
212
+ */
213
+ async searchPastFixes(query: string): Promise<DejaVuMatch[]> {
214
+ const matches: DejaVuMatch[] = [];
215
+
216
+ // Check if query looks like an error
217
+ if (!this.looksLikeError(query)) {
218
+ return matches;
219
+ }
220
+
221
+ try {
222
+ // Search for similar error queries
223
+ const stmt = this.db.prepare(`
224
+ SELECT ue.query, ue.file_path, ue.timestamp, qp.result_files
225
+ FROM usage_events ue
226
+ LEFT JOIN query_patterns qp ON ue.query = qp.query_text
227
+ WHERE ue.event_type = 'query'
228
+ AND (ue.query LIKE '%error%' OR ue.query LIKE '%fix%' OR ue.query LIKE '%bug%')
229
+ AND ue.timestamp > unixepoch() - ? * 86400
230
+ ORDER BY ue.timestamp DESC
231
+ LIMIT 100
232
+ `);
233
+
234
+ const rows = stmt.all(this.MAX_AGE_DAYS) as Array<{
235
+ query: string;
236
+ file_path: string | null;
237
+ timestamp: number;
238
+ result_files: string | null;
239
+ }>;
240
+
241
+ for (const row of rows) {
242
+ if (!row.query) continue;
243
+
244
+ const similarity = this.calculateTextSimilarity(query, row.query);
245
+
246
+ if (similarity >= this.SIMILARITY_THRESHOLD * 0.8) { // Slightly lower threshold for errors
247
+ const resultFiles = row.result_files ? this.parseJsonArray(row.result_files) : [];
248
+ const file = row.file_path || resultFiles[0] || 'unknown';
249
+
250
+ matches.push({
251
+ type: 'fix',
252
+ similarity,
253
+ when: new Date(row.timestamp * 1000),
254
+ file,
255
+ snippet: row.query.slice(0, 100),
256
+ message: formatTimeAgoWithContext(new Date(row.timestamp * 1000), 'encountered a similar issue', file),
257
+ });
258
+ }
259
+ }
260
+ } catch (error) {
261
+ console.error('Error searching past fixes:', error);
262
+ }
263
+
264
+ return matches;
265
+ }
266
+
267
+ /**
268
+ * Record that a query was made (for future déjà vu detection)
269
+ */
270
+ recordQuery(query: string, files: string[], wasUseful?: boolean): void {
271
+ try {
272
+ const queryHash = this.hashQuery(query);
273
+
274
+ const stmt = this.db.prepare(`
275
+ INSERT INTO query_patterns (query_hash, query_text, result_files, hit_count, last_used)
276
+ VALUES (?, ?, ?, 1, unixepoch())
277
+ ON CONFLICT(query_hash) DO UPDATE SET
278
+ hit_count = hit_count + 1,
279
+ last_used = unixepoch()
280
+ `);
281
+
282
+ stmt.run(queryHash, query, JSON.stringify(files));
283
+
284
+ // Update usefulness if provided
285
+ if (wasUseful !== undefined) {
286
+ this.updateUsefulness(queryHash, wasUseful);
287
+ }
288
+ } catch (error) {
289
+ console.error('Error recording query:', error);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Update usefulness score for a query
295
+ */
296
+ updateUsefulness(queryHashOrText: string, wasUseful: boolean): void {
297
+ try {
298
+ const queryHash = queryHashOrText.length === 16
299
+ ? queryHashOrText
300
+ : this.hashQuery(queryHashOrText);
301
+
302
+ // Exponential moving average
303
+ const alpha = 0.3;
304
+ const newScore = wasUseful ? 1.0 : 0.0;
305
+
306
+ const stmt = this.db.prepare(`
307
+ UPDATE query_patterns
308
+ SET avg_usefulness = avg_usefulness * (1 - ?) + ? * ?
309
+ WHERE query_hash = ?
310
+ `);
311
+
312
+ stmt.run(alpha, alpha, newScore, queryHash);
313
+ } catch (error) {
314
+ console.error('Error updating usefulness:', error);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Get déjà vu statistics
320
+ */
321
+ getStats(): { totalQueries: number; usefulQueries: number; avgUsefulness: number } {
322
+ try {
323
+ const stmt = this.db.prepare(`
324
+ SELECT
325
+ COUNT(*) as total,
326
+ SUM(CASE WHEN avg_usefulness >= ? THEN 1 ELSE 0 END) as useful,
327
+ AVG(avg_usefulness) as avg_useful
328
+ FROM query_patterns
329
+ WHERE last_used > unixepoch() - ? * 86400
330
+ `);
331
+
332
+ const result = stmt.get(this.MIN_USEFULNESS, this.MAX_AGE_DAYS) as {
333
+ total: number;
334
+ useful: number;
335
+ avg_useful: number;
336
+ };
337
+
338
+ return {
339
+ totalQueries: result.total || 0,
340
+ usefulQueries: result.useful || 0,
341
+ avgUsefulness: result.avg_useful || 0,
342
+ };
343
+ } catch {
344
+ return { totalQueries: 0, usefulQueries: 0, avgUsefulness: 0 };
345
+ }
346
+ }
347
+
348
+ // ========== Private Methods ==========
349
+
350
+ private calculateTextSimilarity(a: string, b: string): number {
351
+ // Normalize texts
352
+ const normalizeText = (text: string) =>
353
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 2);
354
+
355
+ const wordsA = new Set(normalizeText(a));
356
+ const wordsB = new Set(normalizeText(b));
357
+
358
+ if (wordsA.size === 0 || wordsB.size === 0) {
359
+ return 0;
360
+ }
361
+
362
+ // Jaccard similarity
363
+ let intersection = 0;
364
+ for (const word of wordsA) {
365
+ if (wordsB.has(word)) {
366
+ intersection++;
367
+ }
368
+ }
369
+
370
+ const union = wordsA.size + wordsB.size - intersection;
371
+ return intersection / union;
372
+ }
373
+
374
+ private looksLikeError(query: string): boolean {
375
+ const errorPatterns = [
376
+ /error/i,
377
+ /exception/i,
378
+ /failed/i,
379
+ /failing/i,
380
+ /broken/i,
381
+ /fix/i,
382
+ /bug/i,
383
+ /issue/i,
384
+ /not working/i,
385
+ /doesn't work/i,
386
+ /crash/i,
387
+ /undefined/i,
388
+ /null/i,
389
+ /NaN/i,
390
+ /TypeError/i,
391
+ /ReferenceError/i,
392
+ /SyntaxError/i,
393
+ ];
394
+
395
+ return errorPatterns.some(pattern => pattern.test(query));
396
+ }
397
+
398
+ private getRecencyScore(date: Date): number {
399
+ const now = new Date();
400
+ const diffMs = now.getTime() - date.getTime();
401
+ const diffDays = diffMs / (1000 * 60 * 60 * 24);
402
+
403
+ // Exponential decay: score of 1 for today, approaching 0 at MAX_AGE_DAYS
404
+ return Math.exp(-diffDays / (this.MAX_AGE_DAYS / 3));
405
+ }
406
+
407
+ private parseJsonArray(json: string | null): string[] {
408
+ if (!json) return [];
409
+ try {
410
+ const parsed = JSON.parse(json);
411
+ return Array.isArray(parsed) ? parsed : [];
412
+ } catch {
413
+ return [];
414
+ }
415
+ }
416
+
417
+ private hashQuery(query: string): string {
418
+ const normalized = query.toLowerCase().trim().replace(/\s+/g, ' ');
419
+ return createHash('md5').update(normalized).digest('hex').slice(0, 16);
420
+ }
421
+ }