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,485 @@
1
+ import { createHash } from 'crypto';
2
+ import type Database from 'better-sqlite3';
3
+ import type { SearchResult } from '../types/index.js';
4
+
5
+ export type UsageEventType =
6
+ | 'query' // User made a query
7
+ | 'file_view' // File was viewed
8
+ | 'context_used' // Context was included in response
9
+ | 'context_ignored' // Context was retrieved but not used
10
+ | 'decision_made' // User made a decision
11
+ | 'file_edit'; // User edited a file
12
+
13
+ interface UsageEvent {
14
+ eventType: UsageEventType;
15
+ filePath?: string;
16
+ query?: string;
17
+ contextUsed?: boolean;
18
+ }
19
+
20
+ interface FileAccessStats {
21
+ fileId: number;
22
+ accessCount: number;
23
+ lastAccessed: number;
24
+ relevanceScore: number;
25
+ }
26
+
27
+ interface QueryPattern {
28
+ queryHash: string;
29
+ queryText: string;
30
+ resultFiles: string[];
31
+ hitCount: number;
32
+ avgUsefulness: number;
33
+ }
34
+
35
+ export class LearningEngine {
36
+ private db: Database.Database;
37
+ private hotCache: Map<string, { content: string; accessCount: number; lastAccessed: number }> = new Map();
38
+ private readonly MAX_CACHE_SIZE = 50;
39
+ private readonly CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
40
+
41
+ constructor(db: Database.Database) {
42
+ this.db = db;
43
+ }
44
+
45
+ // Track usage events
46
+ trackEvent(event: UsageEvent): void {
47
+ const stmt = this.db.prepare(`
48
+ INSERT INTO usage_events (event_type, file_path, query, context_used, timestamp)
49
+ VALUES (?, ?, ?, ?, unixepoch())
50
+ `);
51
+ stmt.run(
52
+ event.eventType,
53
+ event.filePath || null,
54
+ event.query || null,
55
+ event.contextUsed ? 1 : 0
56
+ );
57
+
58
+ // Update file access stats if file was accessed
59
+ if (event.filePath && (event.eventType === 'file_view' || event.eventType === 'context_used')) {
60
+ this.updateFileAccess(event.filePath);
61
+ }
62
+ }
63
+
64
+ // Track a query and its results for pattern learning
65
+ trackQuery(query: string, resultFiles: string[]): void {
66
+ const queryHash = this.hashQuery(query);
67
+
68
+ const stmt = this.db.prepare(`
69
+ INSERT INTO query_patterns (query_hash, query_text, result_files, hit_count, last_used)
70
+ VALUES (?, ?, ?, 1, unixepoch())
71
+ ON CONFLICT(query_hash) DO UPDATE SET
72
+ hit_count = hit_count + 1,
73
+ last_used = unixepoch()
74
+ `);
75
+ stmt.run(queryHash, query, JSON.stringify(resultFiles));
76
+ }
77
+
78
+ // Update usefulness score for a query pattern
79
+ updateQueryUsefulness(query: string, wasUseful: boolean): void {
80
+ const queryHash = this.hashQuery(query);
81
+
82
+ // Exponential moving average for usefulness
83
+ const alpha = 0.3;
84
+ const newScore = wasUseful ? 1.0 : 0.0;
85
+
86
+ const stmt = this.db.prepare(`
87
+ UPDATE query_patterns
88
+ SET avg_usefulness = avg_usefulness * (1 - ?) + ? * ?
89
+ WHERE query_hash = ?
90
+ `);
91
+ stmt.run(alpha, alpha, newScore, queryHash);
92
+ }
93
+
94
+ // Update file access statistics
95
+ private updateFileAccess(filePath: string): void {
96
+ // Get file ID
97
+ const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
98
+ const file = fileStmt.get(filePath) as { id: number } | undefined;
99
+
100
+ if (!file) return;
101
+
102
+ const stmt = this.db.prepare(`
103
+ INSERT INTO file_access (file_id, access_count, last_accessed, relevance_score)
104
+ VALUES (?, 1, unixepoch(), 0.5)
105
+ ON CONFLICT(file_id) DO UPDATE SET
106
+ access_count = access_count + 1,
107
+ last_accessed = unixepoch(),
108
+ relevance_score = MIN(1.0, relevance_score + 0.05)
109
+ `);
110
+ stmt.run(file.id);
111
+
112
+ // Update hot cache
113
+ this.updateHotCache(filePath);
114
+ }
115
+
116
+ // Get personalized boost for a file based on usage patterns
117
+ getPersonalizedBoost(filePath: string): number {
118
+ const fileStmt = this.db.prepare('SELECT id FROM files WHERE path = ?');
119
+ const file = fileStmt.get(filePath) as { id: number } | undefined;
120
+
121
+ if (!file) return 1.0;
122
+
123
+ const stmt = this.db.prepare(`
124
+ SELECT access_count, last_accessed, relevance_score
125
+ FROM file_access
126
+ WHERE file_id = ?
127
+ `);
128
+ const stats = stmt.get(file.id) as FileAccessStats | undefined;
129
+
130
+ if (!stats) return 1.0;
131
+
132
+ // Calculate boost based on:
133
+ // 1. Access frequency (log scale to avoid extreme boosts)
134
+ // 2. Recency (decay over time)
135
+ // 3. Learned relevance score
136
+
137
+ const frequencyBoost = 1 + Math.log10(1 + stats.accessCount) * 0.2;
138
+
139
+ const hoursSinceAccess = (Date.now() / 1000 - stats.lastAccessed) / 3600;
140
+ const recencyBoost = Math.exp(-hoursSinceAccess / 168); // Decay over 1 week
141
+
142
+ const relevanceBoost = 0.5 + stats.relevanceScore;
143
+
144
+ return frequencyBoost * (0.7 + 0.3 * recencyBoost) * relevanceBoost;
145
+ }
146
+
147
+ // Apply personalized ranking to search results
148
+ applyPersonalizedRanking(results: SearchResult[]): SearchResult[] {
149
+ return results
150
+ .map(r => ({
151
+ ...r,
152
+ score: (r.score || r.similarity) * this.getPersonalizedBoost(r.file)
153
+ }))
154
+ .sort((a, b) => (b.score || 0) - (a.score || 0));
155
+ }
156
+
157
+ // Get frequently accessed files for pre-fetching
158
+ getFrequentFiles(limit: number = 20): string[] {
159
+ const stmt = this.db.prepare(`
160
+ SELECT f.path
161
+ FROM file_access fa
162
+ JOIN files f ON fa.file_id = f.id
163
+ ORDER BY fa.access_count DESC, fa.last_accessed DESC
164
+ LIMIT ?
165
+ `);
166
+ const rows = stmt.all(limit) as { path: string }[];
167
+ return rows.map(r => r.path);
168
+ }
169
+
170
+ // Get recently accessed files
171
+ getRecentFiles(limit: number = 10): string[] {
172
+ const stmt = this.db.prepare(`
173
+ SELECT f.path
174
+ FROM file_access fa
175
+ JOIN files f ON fa.file_id = f.id
176
+ ORDER BY fa.last_accessed DESC
177
+ LIMIT ?
178
+ `);
179
+ const rows = stmt.all(limit) as { path: string }[];
180
+ return rows.map(r => r.path);
181
+ }
182
+
183
+ // Predict likely needed files based on current context
184
+ predictNeededFiles(currentFile: string, query: string): string[] {
185
+ const predictions: Set<string> = new Set();
186
+
187
+ // 1. Files frequently accessed together with current file
188
+ const coAccessStmt = this.db.prepare(`
189
+ SELECT DISTINCT ue2.file_path
190
+ FROM usage_events ue1
191
+ JOIN usage_events ue2 ON ABS(ue1.timestamp - ue2.timestamp) < 300
192
+ WHERE ue1.file_path = ?
193
+ AND ue2.file_path != ?
194
+ AND ue2.file_path IS NOT NULL
195
+ GROUP BY ue2.file_path
196
+ ORDER BY COUNT(*) DESC
197
+ LIMIT 5
198
+ `);
199
+ const coAccessed = coAccessStmt.all(currentFile, currentFile) as { file_path: string }[];
200
+ coAccessed.forEach(r => predictions.add(r.file_path));
201
+
202
+ // 2. Files from similar past queries
203
+ const queryHash = this.hashQuery(query);
204
+ const similarQueryStmt = this.db.prepare(`
205
+ SELECT result_files
206
+ FROM query_patterns
207
+ WHERE query_hash = ?
208
+ OR query_text LIKE ?
209
+ ORDER BY avg_usefulness DESC, hit_count DESC
210
+ LIMIT 3
211
+ `);
212
+ const keywords = query.split(/\s+/).slice(0, 3).join('%');
213
+ const similarQueries = similarQueryStmt.all(queryHash, `%${keywords}%`) as { result_files: string }[];
214
+
215
+ for (const q of similarQueries) {
216
+ try {
217
+ const files = JSON.parse(q.result_files) as string[];
218
+ files.slice(0, 3).forEach(f => predictions.add(f));
219
+ } catch {
220
+ // Invalid JSON, skip
221
+ }
222
+ }
223
+
224
+ // 3. Files that import/are imported by current file
225
+ const depStmt = this.db.prepare(`
226
+ SELECT DISTINCT f2.path
227
+ FROM files f1
228
+ JOIN imports i ON i.file_id = f1.id
229
+ JOIN files f2 ON f2.path LIKE '%' || REPLACE(i.imported_from, './', '') || '%'
230
+ WHERE f1.path = ?
231
+ LIMIT 5
232
+ `);
233
+ const deps = depStmt.all(currentFile) as { path: string }[];
234
+ deps.forEach(r => predictions.add(r.path));
235
+
236
+ return Array.from(predictions).slice(0, 10);
237
+ }
238
+
239
+ // Hot cache management
240
+ private updateHotCache(filePath: string): void {
241
+ const existing = this.hotCache.get(filePath);
242
+
243
+ if (existing) {
244
+ existing.accessCount++;
245
+ existing.lastAccessed = Date.now();
246
+ }
247
+
248
+ // Evict old entries if cache is full
249
+ if (this.hotCache.size >= this.MAX_CACHE_SIZE) {
250
+ this.evictCacheEntries();
251
+ }
252
+ }
253
+
254
+ private evictCacheEntries(): void {
255
+ const now = Date.now();
256
+ const entries = Array.from(this.hotCache.entries());
257
+
258
+ // Remove expired entries
259
+ for (const [key, value] of entries) {
260
+ if (now - value.lastAccessed > this.CACHE_TTL_MS) {
261
+ this.hotCache.delete(key);
262
+ }
263
+ }
264
+
265
+ // If still too full, remove least accessed
266
+ if (this.hotCache.size >= this.MAX_CACHE_SIZE) {
267
+ entries
268
+ .sort((a, b) => a[1].accessCount - b[1].accessCount)
269
+ .slice(0, 10)
270
+ .forEach(([key]) => this.hotCache.delete(key));
271
+ }
272
+ }
273
+
274
+ addToHotCache(filePath: string, content: string): void {
275
+ this.hotCache.set(filePath, {
276
+ content,
277
+ accessCount: 1,
278
+ lastAccessed: Date.now()
279
+ });
280
+ this.updateHotCache(filePath);
281
+ }
282
+
283
+ getFromHotCache(filePath: string): string | null {
284
+ const entry = this.hotCache.get(filePath);
285
+ if (entry) {
286
+ entry.accessCount++;
287
+ entry.lastAccessed = Date.now();
288
+ return entry.content;
289
+ }
290
+ return null;
291
+ }
292
+
293
+ isInHotCache(filePath: string): boolean {
294
+ return this.hotCache.has(filePath);
295
+ }
296
+
297
+ getHotCacheStats(): { size: number; files: string[] } {
298
+ return {
299
+ size: this.hotCache.size,
300
+ files: Array.from(this.hotCache.keys())
301
+ };
302
+ }
303
+
304
+ // Query expansion for better retrieval
305
+ expandQuery(query: string): string[] {
306
+ const expansions: string[] = [query];
307
+ const words = query.toLowerCase().split(/\s+/);
308
+
309
+ // Add variations based on common patterns
310
+ const synonyms: Record<string, string[]> = {
311
+ 'auth': ['authentication', 'login', 'authorization'],
312
+ 'db': ['database', 'sql', 'storage'],
313
+ 'api': ['endpoint', 'route', 'handler'],
314
+ 'ui': ['component', 'view', 'frontend'],
315
+ 'error': ['exception', 'failure', 'bug'],
316
+ 'test': ['spec', 'unit', 'integration'],
317
+ 'config': ['configuration', 'settings', 'options'],
318
+ 'user': ['account', 'profile', 'member'],
319
+ 'fix': ['bug', 'issue', 'problem'],
320
+ 'add': ['create', 'implement', 'new'],
321
+ };
322
+
323
+ // Expand synonyms
324
+ for (const word of words) {
325
+ const syns = synonyms[word];
326
+ if (syns) {
327
+ for (const syn of syns) {
328
+ expansions.push(query.replace(new RegExp(word, 'gi'), syn));
329
+ }
330
+ }
331
+ }
332
+
333
+ // Add partial queries for broader search
334
+ if (words.length > 2) {
335
+ expansions.push(words.slice(0, 2).join(' '));
336
+ expansions.push(words.slice(-2).join(' '));
337
+ }
338
+
339
+ return [...new Set(expansions)].slice(0, 5);
340
+ }
341
+
342
+ // Get usage statistics
343
+ getUsageStats(): {
344
+ totalQueries: number;
345
+ totalFileViews: number;
346
+ topFiles: Array<{ path: string; count: number }>;
347
+ recentActivity: number;
348
+ } {
349
+ const queryCountStmt = this.db.prepare(`
350
+ SELECT COUNT(*) as count FROM usage_events WHERE event_type = 'query'
351
+ `);
352
+ const totalQueries = (queryCountStmt.get() as { count: number }).count;
353
+
354
+ const viewCountStmt = this.db.prepare(`
355
+ SELECT COUNT(*) as count FROM usage_events WHERE event_type = 'file_view'
356
+ `);
357
+ const totalFileViews = (viewCountStmt.get() as { count: number }).count;
358
+
359
+ const topFilesStmt = this.db.prepare(`
360
+ SELECT f.path, fa.access_count as count
361
+ FROM file_access fa
362
+ JOIN files f ON fa.file_id = f.id
363
+ ORDER BY fa.access_count DESC
364
+ LIMIT 10
365
+ `);
366
+ const topFiles = topFilesStmt.all() as Array<{ path: string; count: number }>;
367
+
368
+ const recentStmt = this.db.prepare(`
369
+ SELECT COUNT(*) as count
370
+ FROM usage_events
371
+ WHERE timestamp > unixepoch() - 3600
372
+ `);
373
+ const recentActivity = (recentStmt.get() as { count: number }).count;
374
+
375
+ return { totalQueries, totalFileViews, topFiles, recentActivity };
376
+ }
377
+
378
+ // Clean up old usage data
379
+ cleanup(daysToKeep: number = 30): number {
380
+ const stmt = this.db.prepare(`
381
+ DELETE FROM usage_events
382
+ WHERE timestamp < unixepoch() - ? * 86400
383
+ `);
384
+ const result = stmt.run(daysToKeep);
385
+ return result.changes;
386
+ }
387
+
388
+ /**
389
+ * Update importance scores for all files based on usage patterns
390
+ * Called by background intelligence loop
391
+ */
392
+ updateImportanceScores(): void {
393
+ try {
394
+ // Get all files with access stats
395
+ const stmt = this.db.prepare(`
396
+ SELECT f.id, f.path, fa.access_count, fa.last_accessed, fa.relevance_score
397
+ FROM files f
398
+ LEFT JOIN file_access fa ON fa.file_id = f.id
399
+ `);
400
+
401
+ const files = stmt.all() as Array<{
402
+ id: number;
403
+ path: string;
404
+ access_count: number | null;
405
+ last_accessed: number | null;
406
+ relevance_score: number | null;
407
+ }>;
408
+
409
+ // Calculate and update importance scores
410
+ const updateStmt = this.db.prepare(`
411
+ INSERT INTO file_access (file_id, access_count, last_accessed, relevance_score)
412
+ VALUES (?, COALESCE(?, 0), COALESCE(?, unixepoch()), ?)
413
+ ON CONFLICT(file_id) DO UPDATE SET
414
+ relevance_score = excluded.relevance_score
415
+ `);
416
+
417
+ for (const file of files) {
418
+ const importance = this.calculateImportance(
419
+ file.access_count || 0,
420
+ file.last_accessed || Math.floor(Date.now() / 1000),
421
+ file.path
422
+ );
423
+
424
+ updateStmt.run(
425
+ file.id,
426
+ file.access_count,
427
+ file.last_accessed,
428
+ importance
429
+ );
430
+ }
431
+ } catch (error) {
432
+ console.error('Error updating importance scores:', error);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Calculate importance score for a file based on multiple factors
438
+ */
439
+ calculateImportance(accessCount: number, lastAccessed: number, filePath: string): number {
440
+ // Factor 1: Access frequency (log scale to avoid extreme boosts)
441
+ const frequencyScore = Math.log10(1 + accessCount) * 0.4;
442
+
443
+ // Factor 2: Recency (how recently was it accessed)
444
+ const hoursSinceAccess = (Date.now() / 1000 - lastAccessed) / 3600;
445
+ const recencyScore = Math.exp(-hoursSinceAccess / 168) * 0.3; // Decay over 1 week
446
+
447
+ // Factor 3: File importance heuristics
448
+ let fileImportance = 0.3; // Default
449
+
450
+ // Boost important file types
451
+ if (filePath.includes('index.') || filePath.includes('main.')) {
452
+ fileImportance = 0.5;
453
+ } else if (filePath.includes('.config.') || filePath.includes('config/')) {
454
+ fileImportance = 0.45;
455
+ } else if (filePath.includes('.test.') || filePath.includes('.spec.')) {
456
+ fileImportance = 0.25; // Tests slightly less important for context
457
+ } else if (filePath.includes('/types/') || filePath.includes('.d.ts')) {
458
+ fileImportance = 0.4; // Type definitions are often helpful
459
+ }
460
+
461
+ // Combine factors (max 1.0)
462
+ return Math.min(1.0, frequencyScore + recencyScore + fileImportance);
463
+ }
464
+
465
+ /**
466
+ * Get importance score for a specific file
467
+ */
468
+ getFileImportance(filePath: string): number {
469
+ const stmt = this.db.prepare(`
470
+ SELECT fa.relevance_score
471
+ FROM files f
472
+ JOIN file_access fa ON fa.file_id = f.id
473
+ WHERE f.path = ?
474
+ `);
475
+
476
+ const result = stmt.get(filePath) as { relevance_score: number } | undefined;
477
+ return result?.relevance_score || 0.5;
478
+ }
479
+
480
+ private hashQuery(query: string): string {
481
+ // Normalize query for matching
482
+ const normalized = query.toLowerCase().trim().replace(/\s+/g, ' ');
483
+ return createHash('md5').update(normalized).digest('hex').slice(0, 16);
484
+ }
485
+ }