@theihtisham/agent-shadow-brain 1.2.0 → 3.0.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/README.md +837 -73
- package/dist/adapters/aider.d.ts +11 -0
- package/dist/adapters/aider.d.ts.map +1 -0
- package/dist/adapters/aider.js +149 -0
- package/dist/adapters/aider.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/roo-code.d.ts +14 -0
- package/dist/adapters/roo-code.d.ts.map +1 -0
- package/dist/adapters/roo-code.js +186 -0
- package/dist/adapters/roo-code.js.map +1 -0
- package/dist/brain/accessibility-checker.d.ts +10 -0
- package/dist/brain/accessibility-checker.d.ts.map +1 -0
- package/dist/brain/accessibility-checker.js +379 -0
- package/dist/brain/accessibility-checker.js.map +1 -0
- package/dist/brain/adr-engine.d.ts +58 -0
- package/dist/brain/adr-engine.d.ts.map +1 -0
- package/dist/brain/adr-engine.js +400 -0
- package/dist/brain/adr-engine.js.map +1 -0
- package/dist/brain/api-contract-analyzer.d.ts +19 -0
- package/dist/brain/api-contract-analyzer.d.ts.map +1 -0
- package/dist/brain/api-contract-analyzer.js +251 -0
- package/dist/brain/api-contract-analyzer.js.map +1 -0
- package/dist/brain/ast-analyzer.d.ts +23 -0
- package/dist/brain/ast-analyzer.d.ts.map +1 -0
- package/dist/brain/ast-analyzer.js +462 -0
- package/dist/brain/ast-analyzer.js.map +1 -0
- package/dist/brain/code-age-analyzer.d.ts +11 -0
- package/dist/brain/code-age-analyzer.d.ts.map +1 -0
- package/dist/brain/code-age-analyzer.js +152 -0
- package/dist/brain/code-age-analyzer.js.map +1 -0
- package/dist/brain/code-similarity.d.ts +43 -0
- package/dist/brain/code-similarity.d.ts.map +1 -0
- package/dist/brain/code-similarity.js +227 -0
- package/dist/brain/code-similarity.js.map +1 -0
- package/dist/brain/config-drift-detector.d.ts +13 -0
- package/dist/brain/config-drift-detector.d.ts.map +1 -0
- package/dist/brain/config-drift-detector.js +198 -0
- package/dist/brain/config-drift-detector.js.map +1 -0
- package/dist/brain/context-completion.d.ts +39 -0
- package/dist/brain/context-completion.d.ts.map +1 -0
- package/dist/brain/context-completion.js +851 -0
- package/dist/brain/context-completion.js.map +1 -0
- package/dist/brain/dead-code-eliminator.d.ts +16 -0
- package/dist/brain/dead-code-eliminator.d.ts.map +1 -0
- package/dist/brain/dead-code-eliminator.js +359 -0
- package/dist/brain/dead-code-eliminator.js.map +1 -0
- package/dist/brain/dependency-graph.d.ts +35 -0
- package/dist/brain/dependency-graph.d.ts.map +1 -0
- package/dist/brain/dependency-graph.js +310 -0
- package/dist/brain/dependency-graph.js.map +1 -0
- package/dist/brain/env-analyzer.d.ts +13 -0
- package/dist/brain/env-analyzer.d.ts.map +1 -0
- package/dist/brain/env-analyzer.js +277 -0
- package/dist/brain/env-analyzer.js.map +1 -0
- package/dist/brain/i18n-detector.d.ts +12 -0
- package/dist/brain/i18n-detector.d.ts.map +1 -0
- package/dist/brain/i18n-detector.js +242 -0
- package/dist/brain/i18n-detector.js.map +1 -0
- package/dist/brain/learning-engine.d.ts +54 -0
- package/dist/brain/learning-engine.d.ts.map +1 -0
- package/dist/brain/learning-engine.js +855 -0
- package/dist/brain/learning-engine.js.map +1 -0
- package/dist/brain/license-compliance.d.ts +13 -0
- package/dist/brain/license-compliance.d.ts.map +1 -0
- package/dist/brain/license-compliance.js +213 -0
- package/dist/brain/license-compliance.js.map +1 -0
- package/dist/brain/llm-client.d.ts.map +1 -1
- package/dist/brain/llm-client.js +3 -0
- package/dist/brain/llm-client.js.map +1 -1
- package/dist/brain/mcp-server.d.ts +30 -0
- package/dist/brain/mcp-server.d.ts.map +1 -0
- package/dist/brain/mcp-server.js +408 -0
- package/dist/brain/mcp-server.js.map +1 -0
- package/dist/brain/multi-project.d.ts +13 -0
- package/dist/brain/multi-project.d.ts.map +1 -0
- package/dist/brain/multi-project.js +163 -0
- package/dist/brain/multi-project.js.map +1 -0
- package/dist/brain/mutation-advisor.d.ts +11 -0
- package/dist/brain/mutation-advisor.d.ts.map +1 -0
- package/dist/brain/mutation-advisor.js +154 -0
- package/dist/brain/mutation-advisor.js.map +1 -0
- package/dist/brain/neural-mesh.d.ts +69 -0
- package/dist/brain/neural-mesh.d.ts.map +1 -0
- package/dist/brain/neural-mesh.js +677 -0
- package/dist/brain/neural-mesh.js.map +1 -0
- package/dist/brain/orchestrator.d.ts +159 -2
- package/dist/brain/orchestrator.d.ts.map +1 -1
- package/dist/brain/orchestrator.js +478 -0
- package/dist/brain/orchestrator.js.map +1 -1
- package/dist/brain/perf-profiler.d.ts +14 -0
- package/dist/brain/perf-profiler.d.ts.map +1 -0
- package/dist/brain/perf-profiler.js +289 -0
- package/dist/brain/perf-profiler.js.map +1 -0
- package/dist/brain/semantic-analyzer.d.ts +46 -0
- package/dist/brain/semantic-analyzer.d.ts.map +1 -0
- package/dist/brain/semantic-analyzer.js +496 -0
- package/dist/brain/semantic-analyzer.js.map +1 -0
- package/dist/brain/team-mode.d.ts +27 -0
- package/dist/brain/team-mode.d.ts.map +1 -0
- package/dist/brain/team-mode.js +262 -0
- package/dist/brain/team-mode.js.map +1 -0
- package/dist/brain/type-safety.d.ts +13 -0
- package/dist/brain/type-safety.d.ts.map +1 -0
- package/dist/brain/type-safety.js +217 -0
- package/dist/brain/type-safety.js.map +1 -0
- package/dist/cli.js +998 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +379 -2
- package/dist/types.d.ts.map +1 -1
- 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
|