@theihtisham/agent-shadow-brain 1.2.0 → 2.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 (74) hide show
  1. package/README.md +837 -73
  2. package/dist/adapters/aider.d.ts +11 -0
  3. package/dist/adapters/aider.d.ts.map +1 -0
  4. package/dist/adapters/aider.js +149 -0
  5. package/dist/adapters/aider.js.map +1 -0
  6. package/dist/adapters/index.d.ts +3 -1
  7. package/dist/adapters/index.d.ts.map +1 -1
  8. package/dist/adapters/index.js +5 -3
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/roo-code.d.ts +14 -0
  11. package/dist/adapters/roo-code.d.ts.map +1 -0
  12. package/dist/adapters/roo-code.js +186 -0
  13. package/dist/adapters/roo-code.js.map +1 -0
  14. package/dist/brain/adr-engine.d.ts +58 -0
  15. package/dist/brain/adr-engine.d.ts.map +1 -0
  16. package/dist/brain/adr-engine.js +400 -0
  17. package/dist/brain/adr-engine.js.map +1 -0
  18. package/dist/brain/code-similarity.d.ts +43 -0
  19. package/dist/brain/code-similarity.d.ts.map +1 -0
  20. package/dist/brain/code-similarity.js +227 -0
  21. package/dist/brain/code-similarity.js.map +1 -0
  22. package/dist/brain/context-completion.d.ts +39 -0
  23. package/dist/brain/context-completion.d.ts.map +1 -0
  24. package/dist/brain/context-completion.js +851 -0
  25. package/dist/brain/context-completion.js.map +1 -0
  26. package/dist/brain/dependency-graph.d.ts +35 -0
  27. package/dist/brain/dependency-graph.d.ts.map +1 -0
  28. package/dist/brain/dependency-graph.js +310 -0
  29. package/dist/brain/dependency-graph.js.map +1 -0
  30. package/dist/brain/learning-engine.d.ts +54 -0
  31. package/dist/brain/learning-engine.d.ts.map +1 -0
  32. package/dist/brain/learning-engine.js +855 -0
  33. package/dist/brain/learning-engine.js.map +1 -0
  34. package/dist/brain/mcp-server.d.ts +30 -0
  35. package/dist/brain/mcp-server.d.ts.map +1 -0
  36. package/dist/brain/mcp-server.js +408 -0
  37. package/dist/brain/mcp-server.js.map +1 -0
  38. package/dist/brain/multi-project.d.ts +13 -0
  39. package/dist/brain/multi-project.d.ts.map +1 -0
  40. package/dist/brain/multi-project.js +163 -0
  41. package/dist/brain/multi-project.js.map +1 -0
  42. package/dist/brain/neural-mesh.d.ts +69 -0
  43. package/dist/brain/neural-mesh.d.ts.map +1 -0
  44. package/dist/brain/neural-mesh.js +677 -0
  45. package/dist/brain/neural-mesh.js.map +1 -0
  46. package/dist/brain/orchestrator.d.ts +111 -1
  47. package/dist/brain/orchestrator.d.ts.map +1 -1
  48. package/dist/brain/orchestrator.js +302 -0
  49. package/dist/brain/orchestrator.js.map +1 -1
  50. package/dist/brain/perf-profiler.d.ts +14 -0
  51. package/dist/brain/perf-profiler.d.ts.map +1 -0
  52. package/dist/brain/perf-profiler.js +289 -0
  53. package/dist/brain/perf-profiler.js.map +1 -0
  54. package/dist/brain/semantic-analyzer.d.ts +46 -0
  55. package/dist/brain/semantic-analyzer.d.ts.map +1 -0
  56. package/dist/brain/semantic-analyzer.js +496 -0
  57. package/dist/brain/semantic-analyzer.js.map +1 -0
  58. package/dist/brain/team-mode.d.ts +27 -0
  59. package/dist/brain/team-mode.d.ts.map +1 -0
  60. package/dist/brain/team-mode.js +262 -0
  61. package/dist/brain/team-mode.js.map +1 -0
  62. package/dist/brain/type-safety.d.ts +13 -0
  63. package/dist/brain/type-safety.d.ts.map +1 -0
  64. package/dist/brain/type-safety.js +217 -0
  65. package/dist/brain/type-safety.js.map +1 -0
  66. package/dist/cli.js +593 -3
  67. package/dist/cli.js.map +1 -1
  68. package/dist/index.d.ts +15 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +18 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/types.d.ts +228 -0
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +2 -2
@@ -0,0 +1,855 @@
1
+ // src/brain/learning-engine.ts — Self-Improvement Learning Engine for Shadow Brain
2
+ // Learns from past analysis sessions and gets smarter over time.
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import * as os from 'os';
6
+ import * as crypto from 'crypto';
7
+ // ── Learning Engine ─────────────────────────────────────────────────────────
8
+ export class LearningEngine {
9
+ constructor(projectDir, llmClient) {
10
+ this.saveTimer = null;
11
+ this.projectDir = projectDir;
12
+ this.llmClient = llmClient ?? null;
13
+ this.storePath = path.join(os.homedir(), '.shadow-brain', 'learning.json');
14
+ this.store = {
15
+ version: 1,
16
+ lessons: [],
17
+ falsePositiveRates: {},
18
+ codePatterns: [],
19
+ agentPreferences: {},
20
+ projectKnowledge: {},
21
+ lastTraining: new Date(),
22
+ };
23
+ }
24
+ // ── Persistence ─────────────────────────────────────────────────────────
25
+ async load() {
26
+ try {
27
+ const dir = path.dirname(this.storePath);
28
+ await fs.mkdir(dir, { recursive: true });
29
+ const raw = await fs.readFile(this.storePath, 'utf-8');
30
+ const parsed = JSON.parse(raw);
31
+ // Hydrate date strings back to Date objects
32
+ if (parsed.lastTraining)
33
+ parsed.lastTraining = new Date(parsed.lastTraining);
34
+ if (parsed.lessons) {
35
+ parsed.lessons = parsed.lessons.map((l) => ({
36
+ ...l,
37
+ lastSeen: new Date(l.lastSeen),
38
+ }));
39
+ }
40
+ if (parsed.codePatterns) {
41
+ parsed.codePatterns = parsed.codePatterns.map((p) => ({
42
+ ...p,
43
+ lastSeen: new Date(p.lastSeen),
44
+ }));
45
+ }
46
+ if (parsed.projectKnowledge) {
47
+ for (const key of Object.keys(parsed.projectKnowledge)) {
48
+ parsed.projectKnowledge[key].lastUpdated = new Date(parsed.projectKnowledge[key].lastUpdated);
49
+ }
50
+ }
51
+ this.store = {
52
+ version: parsed.version ?? 1,
53
+ lessons: parsed.lessons ?? [],
54
+ falsePositiveRates: parsed.falsePositiveRates ?? {},
55
+ codePatterns: parsed.codePatterns ?? [],
56
+ agentPreferences: parsed.agentPreferences ?? {},
57
+ projectKnowledge: parsed.projectKnowledge ?? {},
58
+ lastTraining: parsed.lastTraining ?? new Date(),
59
+ };
60
+ }
61
+ catch {
62
+ // File doesn't exist or is corrupt — start fresh
63
+ this.store = {
64
+ version: 1,
65
+ lessons: [],
66
+ falsePositiveRates: {},
67
+ codePatterns: [],
68
+ agentPreferences: {},
69
+ projectKnowledge: {},
70
+ lastTraining: new Date(),
71
+ };
72
+ }
73
+ }
74
+ async save() {
75
+ try {
76
+ const dir = path.dirname(this.storePath);
77
+ await fs.mkdir(dir, { recursive: true });
78
+ const data = JSON.stringify(this.store, null, 2);
79
+ await fs.writeFile(this.storePath, data, 'utf-8');
80
+ }
81
+ catch (err) {
82
+ console.error(`[LearningEngine] Failed to save store: ${err.message}`);
83
+ }
84
+ }
85
+ scheduleSave() {
86
+ if (this.saveTimer) {
87
+ clearTimeout(this.saveTimer);
88
+ }
89
+ // Debounce: wait 2 seconds before actually writing to disk
90
+ this.saveTimer = setTimeout(() => {
91
+ this.save().catch((err) => {
92
+ console.error(`[LearningEngine] Debounced save failed: ${err.message}`);
93
+ });
94
+ }, 2000);
95
+ }
96
+ // ── Learning Methods ────────────────────────────────────────────────────
97
+ learnFromInsights(insights, changes) {
98
+ try {
99
+ const now = new Date();
100
+ for (const insight of insights) {
101
+ // Track false positive rates per insight category
102
+ const category = insight.type;
103
+ if (!this.store.falsePositiveRates[category]) {
104
+ this.store.falsePositiveRates[category] = { reported: 0, dismissed: 0 };
105
+ }
106
+ this.store.falsePositiveRates[category].reported++;
107
+ // Create or update lessons based on patterns
108
+ for (const change of changes) {
109
+ const pattern = this.extractPattern(change);
110
+ if (!pattern)
111
+ continue;
112
+ const existingLesson = this.store.lessons.find((l) => l.pattern === pattern && l.category === category);
113
+ if (existingLesson) {
114
+ existingLesson.occurrences++;
115
+ existingLesson.lastSeen = now;
116
+ // Slowly increase confidence when seen repeatedly
117
+ existingLesson.confidence = Math.min(1.0, existingLesson.confidence + 0.02);
118
+ }
119
+ else {
120
+ this.store.lessons.push({
121
+ id: `lesson-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
122
+ category,
123
+ pattern,
124
+ lesson: insight.content.slice(0, 200),
125
+ confidence: 0.3,
126
+ occurrences: 1,
127
+ lastSeen: now,
128
+ source: 'rule',
129
+ });
130
+ }
131
+ }
132
+ // Track code patterns
133
+ for (const change of changes) {
134
+ if (!change.path)
135
+ continue;
136
+ const ext = path.extname(change.path).replace('.', '');
137
+ if (!ext)
138
+ continue;
139
+ const contentPattern = this.extractCodePattern(change.content ?? '');
140
+ if (!contentPattern)
141
+ continue;
142
+ const existingPattern = this.store.codePatterns.find((p) => p.pattern === contentPattern && p.language === ext);
143
+ if (existingPattern) {
144
+ existingPattern.frequency++;
145
+ existingPattern.lastSeen = now;
146
+ if (!existingPattern.associatedInsights.includes(insight.type)) {
147
+ existingPattern.associatedInsights.push(insight.type);
148
+ }
149
+ }
150
+ else {
151
+ this.store.codePatterns.push({
152
+ id: `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
153
+ language: ext,
154
+ pattern: contentPattern,
155
+ description: `Observed in ${path.basename(change.path)}`,
156
+ frequency: 1,
157
+ lastSeen: now,
158
+ associatedInsights: [insight.type],
159
+ });
160
+ }
161
+ }
162
+ }
163
+ this.store.lastTraining = now;
164
+ this.scheduleSave();
165
+ }
166
+ catch (err) {
167
+ console.error(`[LearningEngine] learnFromInsights error: ${err.message}`);
168
+ }
169
+ }
170
+ learnFromFeedback(insightId, accepted) {
171
+ try {
172
+ if (!accepted) {
173
+ // Find the lesson associated with this insight and reduce confidence
174
+ const lesson = this.store.lessons.find((l) => l.lesson.slice(0, 50) === insightId.slice(0, 50));
175
+ if (lesson) {
176
+ lesson.confidence = Math.max(0, lesson.confidence - 0.15);
177
+ }
178
+ // Track in false positive rates
179
+ if (!this.store.falsePositiveRates[insightId]) {
180
+ this.store.falsePositiveRates[insightId] = { reported: 0, dismissed: 0 };
181
+ }
182
+ this.store.falsePositiveRates[insightId].dismissed++;
183
+ }
184
+ this.scheduleSave();
185
+ }
186
+ catch (err) {
187
+ console.error(`[LearningEngine] learnFromFeedback error: ${err.message}`);
188
+ }
189
+ }
190
+ async learnProjectPatterns(context, files) {
191
+ if (!this.llmClient)
192
+ return;
193
+ try {
194
+ const fileSample = files.slice(0, 20).map((f) => path.basename(f)).join(', ');
195
+ const langList = context.language.join(', ');
196
+ const prompt = `Analyze this project and extract conventions, patterns, and architecture insights.
197
+
198
+ Project: ${context.name}
199
+ Languages: ${langList}
200
+ Framework: ${context.framework ?? 'unknown'}
201
+ Files sample: ${fileSample}
202
+ Directory structure: ${context.structure.slice(0, 30).join('\n')}
203
+
204
+ Return a JSON object with these fields:
205
+ {
206
+ "conventions": ["list of coding conventions observed, e.g. 'uses arrow functions', 'snake_case file names'"],
207
+ "architecture": "brief description of the architecture pattern used",
208
+ "commonPatterns": ["common code patterns found in this project"],
209
+ "avoidPatterns": ["patterns or practices this project avoids"],
210
+ "dependencies": ["key dependencies this project relies on"]
211
+ }`;
212
+ const systemPrompt = 'You are a codebase analysis expert. Extract patterns and conventions from project metadata. Return valid JSON only.';
213
+ let result;
214
+ try {
215
+ result = await this.llmClient.completeWithSchema(prompt,
216
+ // Minimal inline schema since we can't use zod here without importing it
217
+ // The LLMClient.completeWithSchema needs a ZodSchema, so we'll parse manually
218
+ null, systemPrompt);
219
+ }
220
+ catch {
221
+ // Manual JSON parse fallback
222
+ const raw = await this.llmClient.complete(prompt, systemPrompt);
223
+ const cleaned = raw.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
224
+ result = JSON.parse(cleaned);
225
+ }
226
+ const knowledge = {
227
+ name: context.name,
228
+ conventions: result.conventions ?? [],
229
+ architecture: result.architecture ?? '',
230
+ commonPatterns: result.commonPatterns ?? [],
231
+ avoidPatterns: result.avoidPatterns ?? [],
232
+ dependencies: result.dependencies ?? [],
233
+ lastUpdated: new Date(),
234
+ };
235
+ this.store.projectKnowledge[context.rootDir] = knowledge;
236
+ this.scheduleSave();
237
+ }
238
+ catch (err) {
239
+ console.error(`[LearningEngine] learnProjectPatterns error: ${err.message}`);
240
+ }
241
+ }
242
+ // ── Retrieval Methods ───────────────────────────────────────────────────
243
+ getRelevantLessons(changes) {
244
+ const now = new Date();
245
+ const relevant = [];
246
+ for (const change of changes) {
247
+ const pattern = this.extractPattern(change);
248
+ if (!pattern)
249
+ continue;
250
+ for (const lesson of this.store.lessons) {
251
+ // Match by pattern similarity or file extension
252
+ if (lesson.pattern === pattern ||
253
+ change.path.endsWith(`.${lesson.pattern.split(':')[0]}`) ||
254
+ lesson.pattern.includes(path.extname(change.path).replace('.', ''))) {
255
+ // Boost confidence for recently seen lessons
256
+ const daysSinceLastSeen = (now.getTime() - new Date(lesson.lastSeen).getTime()) / (1000 * 60 * 60 * 24);
257
+ const recencyBoost = Math.max(0, 1 - daysSinceLastSeen / 30) * 0.1;
258
+ relevant.push({
259
+ ...lesson,
260
+ confidence: Math.min(1.0, lesson.confidence + recencyBoost),
261
+ });
262
+ }
263
+ }
264
+ }
265
+ // Sort by confidence descending, deduplicate
266
+ const seen = new Set();
267
+ return relevant
268
+ .sort((a, b) => b.confidence - a.confidence)
269
+ .filter((l) => {
270
+ if (seen.has(l.id))
271
+ return false;
272
+ seen.add(l.id);
273
+ return true;
274
+ })
275
+ .slice(0, 20);
276
+ }
277
+ getProjectKnowledge(projectDir) {
278
+ return this.store.projectKnowledge[projectDir] ?? null;
279
+ }
280
+ async getIntelligenceBoost(changes, context) {
281
+ const insights = [];
282
+ try {
283
+ // 1. Rule-based insights from learned lessons
284
+ const relevantLessons = this.getRelevantLessons(changes);
285
+ for (const lesson of relevantLessons) {
286
+ if (lesson.confidence < 0.5)
287
+ continue;
288
+ insights.push({
289
+ type: 'pattern',
290
+ priority: lesson.confidence > 0.8 ? 'high' : lesson.confidence > 0.6 ? 'medium' : 'low',
291
+ title: `Learned: ${lesson.category} pattern detected`,
292
+ content: lesson.lesson,
293
+ files: changes.map((c) => c.path),
294
+ timestamp: new Date(),
295
+ });
296
+ }
297
+ // 2. Knowledge-based insights from project conventions
298
+ const knowledge = this.getProjectKnowledge(context.rootDir);
299
+ if (knowledge) {
300
+ for (const change of changes) {
301
+ if (!change.content)
302
+ continue;
303
+ // Check if changes violate known avoid patterns
304
+ for (const avoid of knowledge.avoidPatterns) {
305
+ const patternLower = avoid.toLowerCase();
306
+ const contentLower = change.content.toLowerCase();
307
+ if (contentLower.includes(patternLower)) {
308
+ insights.push({
309
+ type: 'warning',
310
+ priority: 'medium',
311
+ title: `Project convention violation: ${avoid}`,
312
+ content: `This project avoids "${avoid}". Consider refactoring in ${path.basename(change.path)}.`,
313
+ files: [change.path],
314
+ timestamp: new Date(),
315
+ });
316
+ }
317
+ }
318
+ }
319
+ // Check for missing conventions
320
+ for (const convention of knowledge.conventions) {
321
+ const filesWithoutConvention = changes.filter((c) => {
322
+ if (!c.content)
323
+ return false;
324
+ return !this.contentFollowsConvention(c.content, convention);
325
+ });
326
+ if (filesWithoutConvention.length > 0) {
327
+ insights.push({
328
+ type: 'suggestion',
329
+ priority: 'low',
330
+ title: `Convention check: ${convention}`,
331
+ content: `Some files may not follow project convention "${convention}": ${filesWithoutConvention.map((f) => path.basename(f.path)).join(', ')}`,
332
+ files: filesWithoutConvention.map((f) => f.path),
333
+ timestamp: new Date(),
334
+ });
335
+ }
336
+ }
337
+ }
338
+ // 3. LLM-powered super insights (if client available)
339
+ if (this.llmClient && changes.length > 0) {
340
+ const llmInsights = await this.generateLLMInsights(changes, context, knowledge);
341
+ insights.push(...llmInsights);
342
+ }
343
+ }
344
+ catch (err) {
345
+ console.error(`[LearningEngine] getIntelligenceBoost error: ${err.message}`);
346
+ }
347
+ return insights;
348
+ }
349
+ getStats() {
350
+ const lessons = this.store.lessons;
351
+ const totalConfidence = lessons.reduce((sum, l) => sum + l.confidence, 0);
352
+ const avgConfidence = lessons.length > 0 ? totalConfidence / lessons.length : 0;
353
+ return {
354
+ totalLessons: lessons.length,
355
+ totalPatterns: this.store.codePatterns.length,
356
+ avgConfidence: Math.round(avgConfidence * 100) / 100,
357
+ projectCount: Object.keys(this.store.projectKnowledge).length,
358
+ };
359
+ }
360
+ // ── Orchestrator-Facing Methods ────────────────────────────────────────────
361
+ /**
362
+ * learnFromProject — Scan the project for patterns and extract lessons.
363
+ * Called by the orchestrator during a learning cycle.
364
+ */
365
+ async learnFromProject() {
366
+ try {
367
+ const srcDir = path.join(this.projectDir, 'src');
368
+ const scanDir = await this.dirExists(srcDir) ? srcDir : this.projectDir;
369
+ const sourceFiles = await this.collectSourceFiles(scanDir, 100);
370
+ if (sourceFiles.length === 0)
371
+ return;
372
+ const now = new Date();
373
+ for (const filePath of sourceFiles) {
374
+ try {
375
+ const content = await fs.readFile(filePath, 'utf-8');
376
+ const ext = path.extname(filePath).replace('.', '');
377
+ // Extract code patterns from each file
378
+ const patternKey = this.extractCodePattern(content);
379
+ if (!patternKey)
380
+ continue;
381
+ // Record or update each individual pattern
382
+ const individualPatterns = patternKey.split('+');
383
+ for (const pat of individualPatterns) {
384
+ const existing = this.store.codePatterns.find((p) => p.pattern === pat && p.language === ext);
385
+ if (existing) {
386
+ existing.frequency++;
387
+ existing.lastSeen = now;
388
+ }
389
+ else {
390
+ this.store.codePatterns.push({
391
+ id: `pattern-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
392
+ language: ext,
393
+ pattern: pat,
394
+ description: `Observed in ${path.relative(this.projectDir, filePath)}`,
395
+ frequency: 1,
396
+ lastSeen: now,
397
+ associatedInsights: [],
398
+ });
399
+ }
400
+ }
401
+ // Create lessons for notable patterns
402
+ this.createLessonFromContent(content, filePath, ext, now);
403
+ }
404
+ catch {
405
+ // Skip unreadable files
406
+ }
407
+ }
408
+ // If LLM is available, enrich lessons with AI analysis
409
+ if (this.llmClient && this.store.codePatterns.length > 0) {
410
+ try {
411
+ await this.enrichLessonsWithLLM();
412
+ }
413
+ catch (err) {
414
+ console.error(`[LearningEngine] LLM enrichment failed: ${err.message}`);
415
+ }
416
+ }
417
+ this.store.lastTraining = now;
418
+ this.scheduleSave();
419
+ }
420
+ catch (err) {
421
+ console.error(`[LearningEngine] learnFromProject error: ${err.message}`);
422
+ }
423
+ }
424
+ /**
425
+ * getLessons — Return all learned lessons in the format the orchestrator expects.
426
+ */
427
+ async getLessons() {
428
+ return this.store.lessons
429
+ .sort((a, b) => b.confidence - a.confidence)
430
+ .map((l) => ({
431
+ category: l.category,
432
+ pattern: l.pattern,
433
+ lesson: l.lesson,
434
+ confidence: l.confidence,
435
+ }));
436
+ }
437
+ /**
438
+ * recordInsight — Record a BrainInsight as a learned lesson.
439
+ * Called by the orchestrator when critical/high-priority insights are generated.
440
+ */
441
+ async recordInsight(insight) {
442
+ try {
443
+ const now = new Date();
444
+ // Create a lesson from the insight
445
+ const pattern = insight.files && insight.files.length > 0
446
+ ? `${path.extname(insight.files[0]).replace('.', '') || 'general'}:${insight.type}`
447
+ : `general:${insight.type}`;
448
+ const existing = this.store.lessons.find((l) => l.pattern === pattern && l.lesson === insight.content.slice(0, 300));
449
+ if (existing) {
450
+ existing.occurrences++;
451
+ existing.lastSeen = now;
452
+ // Boost confidence slightly for repeated observations
453
+ existing.confidence = Math.min(1.0, existing.confidence + 0.05);
454
+ }
455
+ else {
456
+ this.store.lessons.push({
457
+ id: `lesson-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
458
+ category: insight.type,
459
+ pattern,
460
+ lesson: insight.content.slice(0, 300),
461
+ confidence: insight.priority === 'critical' ? 0.7 : insight.priority === 'high' ? 0.5 : 0.3,
462
+ occurrences: 1,
463
+ lastSeen: now,
464
+ source: 'rule',
465
+ });
466
+ }
467
+ // Update false positive tracking
468
+ if (!this.store.falsePositiveRates[insight.type]) {
469
+ this.store.falsePositiveRates[insight.type] = { reported: 0, dismissed: 0 };
470
+ }
471
+ this.store.falsePositiveRates[insight.type].reported++;
472
+ this.store.lastTraining = now;
473
+ this.scheduleSave();
474
+ }
475
+ catch (err) {
476
+ console.error(`[LearningEngine] recordInsight error: ${err.message}`);
477
+ }
478
+ }
479
+ // ── Cleanup ─────────────────────────────────────────────────────────────
480
+ cleanup() {
481
+ try {
482
+ const now = new Date();
483
+ const sixtyDaysAgo = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
484
+ // Remove low-confidence lessons
485
+ this.store.lessons = this.store.lessons.filter((l) => l.confidence >= 0.1);
486
+ // Remove stale code patterns (older than 60 days)
487
+ this.store.codePatterns = this.store.codePatterns.filter((p) => new Date(p.lastSeen) > sixtyDaysAgo);
488
+ this.scheduleSave();
489
+ }
490
+ catch (err) {
491
+ console.error(`[LearningEngine] cleanup error: ${err.message}`);
492
+ }
493
+ }
494
+ // ── Private Helpers ─────────────────────────────────────────────────────
495
+ async dirExists(dirPath) {
496
+ try {
497
+ const stat = await fs.stat(dirPath);
498
+ return stat.isDirectory();
499
+ }
500
+ catch {
501
+ return false;
502
+ }
503
+ }
504
+ async collectSourceFiles(dir, limit) {
505
+ const results = [];
506
+ const skipDirs = new Set([
507
+ 'node_modules', '.git', 'dist', 'build', 'coverage',
508
+ '.next', '.nuxt', '.cache', '.turbo', '__pycache__',
509
+ 'target', 'vendor', '.shadow-brain',
510
+ ]);
511
+ const sourceExtensions = new Set([
512
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
513
+ '.py', '.go', '.rs', '.java', '.rb',
514
+ ]);
515
+ const queue = [dir];
516
+ while (queue.length > 0 && results.length < limit) {
517
+ const current = queue.shift();
518
+ let entries;
519
+ try {
520
+ const dirents = await fs.readdir(current, { withFileTypes: true });
521
+ entries = dirents.map((e) => ({
522
+ name: e.name,
523
+ fullPath: path.join(current, e.name),
524
+ isDir: e.isDirectory(),
525
+ }));
526
+ }
527
+ catch {
528
+ continue;
529
+ }
530
+ for (const entry of entries) {
531
+ if (results.length >= limit)
532
+ break;
533
+ if (entry.isDir) {
534
+ if (!entry.name.startsWith('.') && !skipDirs.has(entry.name)) {
535
+ queue.push(entry.fullPath);
536
+ }
537
+ }
538
+ else {
539
+ const ext = path.extname(entry.name);
540
+ if (sourceExtensions.has(ext)) {
541
+ results.push(entry.fullPath);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ return results;
547
+ }
548
+ createLessonFromContent(content, filePath, ext, now) {
549
+ const relPath = path.relative(this.projectDir, filePath);
550
+ const lessons = [];
551
+ // Security anti-patterns
552
+ if (/\beval\s*\(/.test(content)) {
553
+ lessons.push({
554
+ category: 'security',
555
+ lesson: `Avoid eval() in ${relPath} — it introduces code injection risks`,
556
+ pattern: `eval:${ext}`,
557
+ });
558
+ }
559
+ if (/innerHTML\s*=/.test(content) && !/sanitize|escape|DOMPurify/.test(content)) {
560
+ lessons.push({
561
+ category: 'security',
562
+ lesson: `Direct innerHTML assignment in ${relPath} without sanitization — XSS risk`,
563
+ pattern: `innerHTML:${ext}`,
564
+ });
565
+ }
566
+ // TypeScript quality
567
+ if (ext === 'ts' || ext === 'tsx') {
568
+ const anyMatches = content.match(/:\s*any\b/g);
569
+ if (anyMatches && anyMatches.length > 3) {
570
+ lessons.push({
571
+ category: 'quality',
572
+ lesson: `${relPath} uses 'any' type ${anyMatches.length} times — prefer specific types`,
573
+ pattern: `excessive-any:${ext}`,
574
+ });
575
+ }
576
+ if (/\bas\s+any/.test(content)) {
577
+ lessons.push({
578
+ category: 'quality',
579
+ lesson: `Type assertion 'as any' found in ${relPath} — defeats type safety`,
580
+ pattern: `as-any:${ext}`,
581
+ });
582
+ }
583
+ }
584
+ // Error handling
585
+ if (/catch\s*\(\s*\w*\s*\)\s*\{\s*\}/.test(content)) {
586
+ lessons.push({
587
+ category: 'quality',
588
+ lesson: `Empty catch block in ${relPath} — silently swallowing errors`,
589
+ pattern: `empty-catch:${ext}`,
590
+ });
591
+ }
592
+ // Performance
593
+ if (/\.forEach\s*\(/.test(content) && /await/.test(content)) {
594
+ lessons.push({
595
+ category: 'performance',
596
+ lesson: `Async operations inside forEach in ${relPath} — use for...of or Promise.all instead`,
597
+ pattern: `async-foreach:${ext}`,
598
+ });
599
+ }
600
+ // Maintainability
601
+ const lines = content.split('\n').length;
602
+ if (lines > 500) {
603
+ lessons.push({
604
+ category: 'maintainability',
605
+ lesson: `${relPath} is ${lines} lines long — consider splitting into smaller modules`,
606
+ pattern: `large-file:${ext}`,
607
+ });
608
+ }
609
+ // Add lessons to the store
610
+ for (const { category, lesson, pattern } of lessons) {
611
+ const existing = this.store.lessons.find((l) => l.pattern === pattern && l.category === category);
612
+ if (existing) {
613
+ existing.occurrences++;
614
+ existing.lastSeen = now;
615
+ existing.confidence = Math.min(1.0, existing.confidence + 0.03);
616
+ }
617
+ else {
618
+ this.store.lessons.push({
619
+ id: `lesson-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
620
+ category,
621
+ pattern,
622
+ lesson,
623
+ confidence: 0.4,
624
+ occurrences: 1,
625
+ lastSeen: now,
626
+ source: 'rule',
627
+ });
628
+ }
629
+ }
630
+ }
631
+ async enrichLessonsWithLLM() {
632
+ if (!this.llmClient)
633
+ return;
634
+ // Pick the top patterns to analyze
635
+ const topPatterns = this.store.codePatterns
636
+ .sort((a, b) => b.frequency - a.frequency)
637
+ .slice(0, 10);
638
+ const patternSummary = topPatterns
639
+ .map((p) => `- [${p.language}] ${p.pattern} (frequency: ${p.frequency})`)
640
+ .join('\n');
641
+ const lessonSummary = this.store.lessons
642
+ .slice(0, 10)
643
+ .map((l) => `- [${l.category}] ${l.lesson}`)
644
+ .join('\n');
645
+ const prompt = `Given these code patterns and existing lessons from a project, suggest additional lessons that a senior developer would know.
646
+
647
+ Observed patterns:
648
+ ${patternSummary}
649
+
650
+ Existing lessons:
651
+ ${lessonSummary || 'None yet.'}
652
+
653
+ Return a JSON array of lesson objects:
654
+ [{
655
+ "category": "security" | "performance" | "quality" | "architecture" | "maintainability",
656
+ "pattern": "short pattern key",
657
+ "lesson": "actionable lesson text",
658
+ "confidence": 0.5
659
+ }]
660
+
661
+ Provide 3-5 non-obvious lessons. Do NOT repeat existing ones.`;
662
+ const systemPrompt = 'You are an expert code reviewer. Suggest non-obvious lessons from code patterns. Return valid JSON array only.';
663
+ try {
664
+ const response = await this.llmClient.complete(prompt, systemPrompt);
665
+ const cleaned = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
666
+ let parsed;
667
+ try {
668
+ parsed = JSON.parse(cleaned);
669
+ }
670
+ catch {
671
+ const match = cleaned.match(/\[[\s\S]*\]/);
672
+ if (match) {
673
+ parsed = JSON.parse(match[0]);
674
+ }
675
+ else {
676
+ return;
677
+ }
678
+ }
679
+ if (!Array.isArray(parsed))
680
+ return;
681
+ const now = new Date();
682
+ for (const item of parsed) {
683
+ if (!item.category || !item.pattern || !item.lesson)
684
+ continue;
685
+ const existing = this.store.lessons.find((l) => l.pattern === item.pattern && l.category === item.category);
686
+ if (existing)
687
+ continue; // Don't overwrite
688
+ this.store.lessons.push({
689
+ id: `lesson-llm-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`,
690
+ category: item.category,
691
+ pattern: item.pattern,
692
+ lesson: item.lesson,
693
+ confidence: item.confidence ?? 0.5,
694
+ occurrences: 1,
695
+ lastSeen: now,
696
+ source: 'llm',
697
+ });
698
+ }
699
+ }
700
+ catch {
701
+ // LLM enrichment is best-effort
702
+ }
703
+ }
704
+ extractPattern(change) {
705
+ if (!change.path)
706
+ return null;
707
+ const ext = path.extname(change.path).replace('.', '');
708
+ const type = change.type;
709
+ // Create a pattern key from file extension + change type
710
+ if (ext) {
711
+ return `${ext}:${type}`;
712
+ }
713
+ // Fallback: use directory-based pattern
714
+ const dir = path.dirname(change.path).split(path.sep).slice(-2).join('/');
715
+ return `dir:${dir}:${type}`;
716
+ }
717
+ extractCodePattern(content) {
718
+ if (!content || content.length < 10)
719
+ return null;
720
+ // Detect common patterns in code content
721
+ const patterns = [];
722
+ // Arrow functions
723
+ if (/=>\s*\{/.test(content))
724
+ patterns.push('arrow-fn');
725
+ // async/await
726
+ if (/async\s+/.test(content) && /await\s+/.test(content))
727
+ patterns.push('async-await');
728
+ // try/catch
729
+ if (/try\s*\{/.test(content) && /catch\s*\(/.test(content))
730
+ patterns.push('try-catch');
731
+ // console.log
732
+ if (/console\.log/.test(content))
733
+ patterns.push('console-log');
734
+ // TODO/FIXME
735
+ if (/TODO|FIXME|HACK|XXX/.test(content))
736
+ patterns.push('todo-comment');
737
+ // class-based
738
+ if (/class\s+\w+/.test(content))
739
+ patterns.push('class-based');
740
+ // export default
741
+ if (/export\s+default/.test(content))
742
+ patterns.push('export-default');
743
+ // named exports
744
+ if (/export\s+(const|function|class|interface|type)\s/.test(content))
745
+ patterns.push('named-export');
746
+ // type assertions
747
+ if (/as\s+\w+/.test(content))
748
+ patterns.push('type-assertion');
749
+ // null checks
750
+ if (/[!=]==?\s*null|[!=]==?\s*undefined|\?\.|!\./.test(content))
751
+ patterns.push('null-check');
752
+ // Promises
753
+ if (/new\s+Promise|\.then\(|\.catch\(/.test(content))
754
+ patterns.push('promise');
755
+ // Error throwing
756
+ if (/throw\s+new\s+/.test(content))
757
+ patterns.push('throw-error');
758
+ return patterns.length > 0 ? patterns.join('+') : null;
759
+ }
760
+ contentFollowsConvention(content, convention) {
761
+ const lower = convention.toLowerCase();
762
+ if (lower.includes('arrow function') && !/=>/.test(content))
763
+ return false;
764
+ if (lower.includes('camelcase') && /_\w/.test(content) && !/^[A-Z]/.test(content))
765
+ return true;
766
+ if (lower.includes('snake_case') && /[a-z][A-Z]/.test(content))
767
+ return false;
768
+ if (lower.includes('semicolon') && /[^;{}]\s*$/m.test(content))
769
+ return false;
770
+ // Default: assume it follows
771
+ return true;
772
+ }
773
+ async generateLLMInsights(changes, context, knowledge) {
774
+ if (!this.llmClient)
775
+ return [];
776
+ try {
777
+ const changeSummaries = changes
778
+ .slice(0, 10)
779
+ .map((c) => `- ${c.type} ${c.path}${c.content ? ` (${c.content.split('\n').length} lines)` : ''}`)
780
+ .join('\n');
781
+ const knowledgeContext = knowledge
782
+ ? `Known conventions: ${knowledge.conventions.join(', ')}
783
+ Architecture: ${knowledge.architecture}
784
+ Avoid: ${knowledge.avoidPatterns.join(', ')}`
785
+ : 'No prior knowledge about this project.';
786
+ const lessonsContext = this.store.lessons
787
+ .filter((l) => l.confidence >= 0.6)
788
+ .slice(0, 10)
789
+ .map((l) => `- [${l.category}] ${l.lesson} (confidence: ${l.confidence.toFixed(2)})`)
790
+ .join('\n');
791
+ const prompt = `You are a senior code reviewer AI that has been learning from past code reviews.
792
+
793
+ Analyze these file changes and provide insights that a standard rule-based linter would MISS:
794
+
795
+ Project: ${context.name}
796
+ Languages: ${context.language.join(', ')}
797
+ Framework: ${context.framework ?? 'unknown'}
798
+
799
+ Changes:
800
+ ${changeSummaries}
801
+
802
+ ${knowledgeContext}
803
+
804
+ Previously learned lessons:
805
+ ${lessonsContext || 'No lessons learned yet.'}
806
+
807
+ Return a JSON array of insights. Each insight has:
808
+ {
809
+ "type": "review" | "suggestion" | "warning",
810
+ "priority": "critical" | "high" | "medium" | "low",
811
+ "title": "short title",
812
+ "content": "detailed explanation of the insight with actionable advice",
813
+ "files": ["affected file paths"]
814
+ }
815
+
816
+ Focus on: architectural concerns, maintainability, hidden bugs, performance pitfalls, security gotchas that require human-like understanding.
817
+ Do NOT report obvious linting issues — only things that require deep understanding.`;
818
+ const systemPrompt = 'You are an expert code reviewer AI. Provide deep, non-obvious insights about code changes. Return a valid JSON array. No markdown fences.';
819
+ const response = await this.llmClient.complete(prompt, systemPrompt);
820
+ const cleaned = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
821
+ let parsed;
822
+ try {
823
+ parsed = JSON.parse(cleaned);
824
+ }
825
+ catch {
826
+ // Try to extract JSON array from response
827
+ const match = cleaned.match(/\[[\s\S]*\]/);
828
+ if (match) {
829
+ parsed = JSON.parse(match[0]);
830
+ }
831
+ else {
832
+ return [];
833
+ }
834
+ }
835
+ if (!Array.isArray(parsed))
836
+ return [];
837
+ return parsed
838
+ .filter((item) => item.title && item.content)
839
+ .map((item) => ({
840
+ type: item.type ?? 'suggestion',
841
+ priority: item.priority ?? 'medium',
842
+ title: item.title,
843
+ content: item.content,
844
+ files: Array.isArray(item.files) ? item.files : [],
845
+ timestamp: new Date(),
846
+ }))
847
+ .slice(0, 10);
848
+ }
849
+ catch (err) {
850
+ console.error(`[LearningEngine] LLM insight generation failed: ${err.message}`);
851
+ return [];
852
+ }
853
+ }
854
+ }
855
+ //# sourceMappingURL=learning-engine.js.map