neuronlayer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/index.js +38016 -0
- package/esbuild.config.js +26 -0
- package/package.json +63 -0
- package/src/cli/commands.ts +382 -0
- package/src/core/adr-exporter.ts +253 -0
- package/src/core/architecture/architecture-enforcement.ts +228 -0
- package/src/core/architecture/duplicate-detector.ts +288 -0
- package/src/core/architecture/index.ts +6 -0
- package/src/core/architecture/pattern-learner.ts +306 -0
- package/src/core/architecture/pattern-library.ts +403 -0
- package/src/core/architecture/pattern-validator.ts +324 -0
- package/src/core/change-intelligence/bug-correlator.ts +444 -0
- package/src/core/change-intelligence/change-intelligence.ts +221 -0
- package/src/core/change-intelligence/change-tracker.ts +334 -0
- package/src/core/change-intelligence/fix-suggester.ts +340 -0
- package/src/core/change-intelligence/index.ts +5 -0
- package/src/core/code-verifier.ts +843 -0
- package/src/core/confidence/confidence-scorer.ts +251 -0
- package/src/core/confidence/conflict-checker.ts +289 -0
- package/src/core/confidence/index.ts +5 -0
- package/src/core/confidence/source-tracker.ts +263 -0
- package/src/core/confidence/warning-detector.ts +241 -0
- package/src/core/context-rot/compaction.ts +284 -0
- package/src/core/context-rot/context-health.ts +243 -0
- package/src/core/context-rot/context-rot-prevention.ts +213 -0
- package/src/core/context-rot/critical-context.ts +221 -0
- package/src/core/context-rot/drift-detector.ts +255 -0
- package/src/core/context-rot/index.ts +7 -0
- package/src/core/context.ts +263 -0
- package/src/core/decision-extractor.ts +339 -0
- package/src/core/decisions.ts +69 -0
- package/src/core/deja-vu.ts +421 -0
- package/src/core/engine.ts +1455 -0
- package/src/core/feature-context.ts +726 -0
- package/src/core/ghost-mode.ts +412 -0
- package/src/core/learning.ts +485 -0
- package/src/core/living-docs/activity-tracker.ts +296 -0
- package/src/core/living-docs/architecture-generator.ts +428 -0
- package/src/core/living-docs/changelog-generator.ts +348 -0
- package/src/core/living-docs/component-generator.ts +230 -0
- package/src/core/living-docs/doc-engine.ts +110 -0
- package/src/core/living-docs/doc-validator.ts +282 -0
- package/src/core/living-docs/index.ts +8 -0
- package/src/core/project-manager.ts +297 -0
- package/src/core/summarizer.ts +267 -0
- package/src/core/test-awareness/change-validator.ts +499 -0
- package/src/core/test-awareness/index.ts +5 -0
- package/src/index.ts +49 -0
- package/src/indexing/ast.ts +563 -0
- package/src/indexing/embeddings.ts +85 -0
- package/src/indexing/indexer.ts +245 -0
- package/src/indexing/watcher.ts +78 -0
- package/src/server/gateways/aggregator.ts +374 -0
- package/src/server/gateways/index.ts +473 -0
- package/src/server/gateways/memory-ghost.ts +343 -0
- package/src/server/gateways/memory-query.ts +452 -0
- package/src/server/gateways/memory-record.ts +346 -0
- package/src/server/gateways/memory-review.ts +410 -0
- package/src/server/gateways/memory-status.ts +517 -0
- package/src/server/gateways/memory-verify.ts +392 -0
- package/src/server/gateways/router.ts +434 -0
- package/src/server/gateways/types.ts +610 -0
- package/src/server/mcp.ts +154 -0
- package/src/server/resources.ts +85 -0
- package/src/server/tools.ts +2261 -0
- package/src/storage/database.ts +262 -0
- package/src/storage/tier1.ts +135 -0
- package/src/storage/tier2.ts +764 -0
- package/src/storage/tier3.ts +123 -0
- package/src/types/documentation.ts +619 -0
- package/src/types/index.ts +222 -0
- package/src/utils/config.ts +193 -0
- package/src/utils/files.ts +117 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/tokens.ts +52 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
4
|
+
import type { EmbeddingGenerator } from '../../indexing/embeddings.js';
|
|
5
|
+
import type { Change, Bug, PastBug, Diagnosis } from '../../types/documentation.js';
|
|
6
|
+
import type { ChangeTracker } from './change-tracker.js';
|
|
7
|
+
|
|
8
|
+
// Common error patterns and their likely causes
|
|
9
|
+
const ERROR_PATTERNS = [
|
|
10
|
+
{
|
|
11
|
+
pattern: /cannot read (?:property |properties of )['"]?(\w+)['"]? of (?:undefined|null)/i,
|
|
12
|
+
keywords: ['null check', 'undefined', 'optional chaining', '?.'],
|
|
13
|
+
likelyCause: 'Missing null/undefined check'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
pattern: /(\w+) is not defined/i,
|
|
17
|
+
keywords: ['import', 'require', 'declaration'],
|
|
18
|
+
likelyCause: 'Missing import or variable declaration'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /(\w+) is not a function/i,
|
|
22
|
+
keywords: ['function', 'method', 'call', '()'],
|
|
23
|
+
likelyCause: 'Function doesn\'t exist or wrong type'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /unexpected token/i,
|
|
27
|
+
keywords: ['syntax', 'parse', 'JSON'],
|
|
28
|
+
likelyCause: 'Syntax error or malformed data'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /timeout|timed out/i,
|
|
32
|
+
keywords: ['timeout', 'async', 'await', 'connection'],
|
|
33
|
+
likelyCause: 'Slow operation or connection issue'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /ECONNREFUSED|connection refused/i,
|
|
37
|
+
keywords: ['connection', 'port', 'server', 'database'],
|
|
38
|
+
likelyCause: 'Service not running or wrong port'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /out of memory|heap/i,
|
|
42
|
+
keywords: ['memory', 'leak', 'buffer', 'array'],
|
|
43
|
+
likelyCause: 'Memory leak or large data processing'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /permission denied|EACCES/i,
|
|
47
|
+
keywords: ['permission', 'access', 'chmod', 'sudo'],
|
|
48
|
+
likelyCause: 'Missing permissions'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /module not found|cannot find module/i,
|
|
52
|
+
keywords: ['import', 'require', 'package', 'node_modules'],
|
|
53
|
+
likelyCause: 'Missing dependency or wrong import path'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /type.*is not assignable/i,
|
|
57
|
+
keywords: ['type', 'interface', 'TypeScript'],
|
|
58
|
+
likelyCause: 'Type mismatch'
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export class BugCorrelator {
|
|
63
|
+
private db: Database.Database;
|
|
64
|
+
private changeTracker: ChangeTracker;
|
|
65
|
+
private tier2: Tier2Storage;
|
|
66
|
+
private embeddingGenerator: EmbeddingGenerator;
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
db: Database.Database,
|
|
70
|
+
changeTracker: ChangeTracker,
|
|
71
|
+
tier2: Tier2Storage,
|
|
72
|
+
embeddingGenerator: EmbeddingGenerator
|
|
73
|
+
) {
|
|
74
|
+
this.db = db;
|
|
75
|
+
this.changeTracker = changeTracker;
|
|
76
|
+
this.tier2 = tier2;
|
|
77
|
+
this.embeddingGenerator = embeddingGenerator;
|
|
78
|
+
this.ensureTable();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private ensureTable(): void {
|
|
82
|
+
this.db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS bug_history (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
error TEXT NOT NULL,
|
|
86
|
+
stack_trace TEXT,
|
|
87
|
+
file TEXT,
|
|
88
|
+
line INTEGER,
|
|
89
|
+
timestamp INTEGER NOT NULL,
|
|
90
|
+
status TEXT DEFAULT 'open',
|
|
91
|
+
related_changes TEXT,
|
|
92
|
+
fixed_by TEXT,
|
|
93
|
+
fixed_at INTEGER,
|
|
94
|
+
fix_diff TEXT,
|
|
95
|
+
cause TEXT
|
|
96
|
+
);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_bug_timestamp ON bug_history(timestamp);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_bug_status ON bug_history(status);
|
|
99
|
+
|
|
100
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS bug_fts USING fts5(
|
|
101
|
+
error,
|
|
102
|
+
stack_trace,
|
|
103
|
+
cause,
|
|
104
|
+
content='bug_history',
|
|
105
|
+
content_rowid='rowid'
|
|
106
|
+
);
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Record a bug
|
|
111
|
+
recordBug(error: string, options?: {
|
|
112
|
+
stackTrace?: string;
|
|
113
|
+
file?: string;
|
|
114
|
+
line?: number;
|
|
115
|
+
relatedChanges?: string[];
|
|
116
|
+
}): Bug {
|
|
117
|
+
const id = randomUUID();
|
|
118
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
119
|
+
|
|
120
|
+
this.db.prepare(`
|
|
121
|
+
INSERT INTO bug_history (id, error, stack_trace, file, line, timestamp, status, related_changes)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?, 'open', ?)
|
|
123
|
+
`).run(
|
|
124
|
+
id,
|
|
125
|
+
error,
|
|
126
|
+
options?.stackTrace || null,
|
|
127
|
+
options?.file || null,
|
|
128
|
+
options?.line || null,
|
|
129
|
+
timestamp,
|
|
130
|
+
JSON.stringify(options?.relatedChanges || [])
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
id,
|
|
135
|
+
error,
|
|
136
|
+
stackTrace: options?.stackTrace,
|
|
137
|
+
file: options?.file,
|
|
138
|
+
line: options?.line,
|
|
139
|
+
timestamp: new Date(timestamp * 1000),
|
|
140
|
+
status: 'open',
|
|
141
|
+
relatedChanges: options?.relatedChanges || []
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Record a fix for a bug
|
|
146
|
+
recordFix(bugId: string, fixDiff: string, cause?: string): boolean {
|
|
147
|
+
const result = this.db.prepare(`
|
|
148
|
+
UPDATE bug_history
|
|
149
|
+
SET status = 'fixed', fixed_at = ?, fix_diff = ?, cause = ?
|
|
150
|
+
WHERE id = ?
|
|
151
|
+
`).run(
|
|
152
|
+
Math.floor(Date.now() / 1000),
|
|
153
|
+
fixDiff,
|
|
154
|
+
cause || null,
|
|
155
|
+
bugId
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return result.changes > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Diagnose a bug
|
|
162
|
+
diagnoseBug(error: string, options?: { file?: string; line?: number }): Diagnosis {
|
|
163
|
+
// 1. Extract keywords from error
|
|
164
|
+
const keywords = this.extractKeywords(error);
|
|
165
|
+
|
|
166
|
+
// 2. Find recent changes that might be related
|
|
167
|
+
const recentChanges = this.changeTracker.getRecentChanges(48); // Last 48 hours
|
|
168
|
+
const relevantChanges = this.findRelevantChanges(recentChanges, keywords, options?.file);
|
|
169
|
+
|
|
170
|
+
// 3. Score and rank changes
|
|
171
|
+
const scoredChanges = this.scoreChanges(relevantChanges, error, keywords, options);
|
|
172
|
+
|
|
173
|
+
// 4. Find similar past bugs
|
|
174
|
+
const pastBugs = this.findSimilarBugs(error);
|
|
175
|
+
|
|
176
|
+
// 5. Generate diagnosis
|
|
177
|
+
const likelyCause = scoredChanges.length > 0 ? scoredChanges[0].change : null;
|
|
178
|
+
const confidence = scoredChanges.length > 0 ? scoredChanges[0].score : 0;
|
|
179
|
+
|
|
180
|
+
// 6. Generate reasoning
|
|
181
|
+
const reasoning = this.generateReasoning(error, likelyCause, pastBugs, keywords);
|
|
182
|
+
|
|
183
|
+
// 7. Suggest fix if we have past fixes
|
|
184
|
+
const suggestedFix = this.getSuggestedFix(pastBugs, error);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
likelyCause,
|
|
188
|
+
confidence: Math.round(confidence),
|
|
189
|
+
relatedChanges: scoredChanges.slice(1, 5).map(s => s.change),
|
|
190
|
+
pastSimilarBugs: pastBugs.slice(0, 5),
|
|
191
|
+
suggestedFix,
|
|
192
|
+
reasoning
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private extractKeywords(error: string): string[] {
|
|
197
|
+
const keywords: string[] = [];
|
|
198
|
+
|
|
199
|
+
// Extract from error patterns
|
|
200
|
+
for (const { pattern, keywords: patternKeywords } of ERROR_PATTERNS) {
|
|
201
|
+
if (pattern.test(error)) {
|
|
202
|
+
keywords.push(...patternKeywords);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Extract identifiers (variable/function names)
|
|
207
|
+
const identifiers = error.match(/['"`](\w+)['"`]/g);
|
|
208
|
+
if (identifiers) {
|
|
209
|
+
keywords.push(...identifiers.map(i => i.replace(/['"`]/g, '')));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract file paths
|
|
213
|
+
const paths = error.match(/[\w\-./]+\.(ts|js|tsx|jsx)/g);
|
|
214
|
+
if (paths) {
|
|
215
|
+
keywords.push(...paths);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Extract line numbers
|
|
219
|
+
const lineNums = error.match(/line\s*(\d+)/gi);
|
|
220
|
+
if (lineNums) {
|
|
221
|
+
keywords.push(...lineNums);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Add common error-related words
|
|
225
|
+
const words = error.toLowerCase().split(/\s+/);
|
|
226
|
+
const significantWords = words.filter(w =>
|
|
227
|
+
w.length > 3 &&
|
|
228
|
+
!['error', 'the', 'and', 'for', 'with', 'from'].includes(w)
|
|
229
|
+
);
|
|
230
|
+
keywords.push(...significantWords.slice(0, 10));
|
|
231
|
+
|
|
232
|
+
return [...new Set(keywords)];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private findRelevantChanges(changes: Change[], keywords: string[], file?: string): Change[] {
|
|
236
|
+
return changes.filter(change => {
|
|
237
|
+
// Priority 1: File match
|
|
238
|
+
if (file && change.file.includes(file)) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Priority 2: Keyword in diff or file
|
|
243
|
+
const changeText = `${change.file} ${change.diff} ${change.commitMessage}`.toLowerCase();
|
|
244
|
+
return keywords.some(k => changeText.includes(k.toLowerCase()));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private scoreChanges(
|
|
249
|
+
changes: Change[],
|
|
250
|
+
error: string,
|
|
251
|
+
keywords: string[],
|
|
252
|
+
options?: { file?: string; line?: number }
|
|
253
|
+
): Array<{ change: Change; score: number }> {
|
|
254
|
+
return changes.map(change => {
|
|
255
|
+
let score = 0;
|
|
256
|
+
|
|
257
|
+
// File match (high weight)
|
|
258
|
+
if (options?.file && change.file.includes(options.file)) {
|
|
259
|
+
score += 40;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Recency (more recent = higher score)
|
|
263
|
+
const hoursAgo = (Date.now() - change.timestamp.getTime()) / (1000 * 60 * 60);
|
|
264
|
+
if (hoursAgo < 2) score += 30;
|
|
265
|
+
else if (hoursAgo < 6) score += 20;
|
|
266
|
+
else if (hoursAgo < 24) score += 10;
|
|
267
|
+
|
|
268
|
+
// Keyword matches in diff
|
|
269
|
+
const diffLower = change.diff.toLowerCase();
|
|
270
|
+
let keywordMatches = 0;
|
|
271
|
+
for (const keyword of keywords) {
|
|
272
|
+
if (diffLower.includes(keyword.toLowerCase())) {
|
|
273
|
+
keywordMatches++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
score += Math.min(keywordMatches * 5, 20);
|
|
277
|
+
|
|
278
|
+
// Error pattern match
|
|
279
|
+
for (const { pattern, likelyCause } of ERROR_PATTERNS) {
|
|
280
|
+
if (pattern.test(error)) {
|
|
281
|
+
// Check if diff contains related changes
|
|
282
|
+
if (diffLower.includes('null') || diffLower.includes('undefined') ||
|
|
283
|
+
diffLower.includes('?.') || diffLower.includes('if (')) {
|
|
284
|
+
score += 15;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Deletion score (deletions often cause bugs)
|
|
290
|
+
if (change.linesRemoved > change.linesAdded) {
|
|
291
|
+
score += 10;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { change, score };
|
|
295
|
+
}).sort((a, b) => b.score - a.score);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Find similar bugs from history
|
|
299
|
+
findSimilarBugs(error: string, limit: number = 5): PastBug[] {
|
|
300
|
+
try {
|
|
301
|
+
// Simple text search
|
|
302
|
+
const keywords = error.split(/\s+/)
|
|
303
|
+
.filter(w => w.length > 3)
|
|
304
|
+
.slice(0, 5)
|
|
305
|
+
.join(' OR ');
|
|
306
|
+
|
|
307
|
+
if (!keywords) return [];
|
|
308
|
+
|
|
309
|
+
const rows = this.db.prepare(`
|
|
310
|
+
SELECT id, error, cause, fix_diff, file, timestamp, status
|
|
311
|
+
FROM bug_history
|
|
312
|
+
WHERE status = 'fixed' AND (
|
|
313
|
+
error LIKE ? OR
|
|
314
|
+
error LIKE ? OR
|
|
315
|
+
error LIKE ?
|
|
316
|
+
)
|
|
317
|
+
ORDER BY timestamp DESC
|
|
318
|
+
LIMIT ?
|
|
319
|
+
`).all(
|
|
320
|
+
`%${error.slice(0, 30)}%`,
|
|
321
|
+
`%${error.split(' ')[0]}%`,
|
|
322
|
+
`%${error.split(':')[0]}%`,
|
|
323
|
+
limit
|
|
324
|
+
) as Array<{
|
|
325
|
+
id: string;
|
|
326
|
+
error: string;
|
|
327
|
+
cause: string | null;
|
|
328
|
+
fix_diff: string | null;
|
|
329
|
+
file: string | null;
|
|
330
|
+
timestamp: number;
|
|
331
|
+
status: string;
|
|
332
|
+
}>;
|
|
333
|
+
|
|
334
|
+
return rows.map(row => ({
|
|
335
|
+
id: row.id,
|
|
336
|
+
error: row.error,
|
|
337
|
+
cause: row.cause || undefined,
|
|
338
|
+
fix: row.cause || undefined,
|
|
339
|
+
fixDiff: row.fix_diff || undefined,
|
|
340
|
+
file: row.file || undefined,
|
|
341
|
+
date: new Date(row.timestamp * 1000),
|
|
342
|
+
similarity: this.calculateSimilarity(error, row.error)
|
|
343
|
+
})).filter(bug => bug.similarity > 30);
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private calculateSimilarity(error1: string, error2: string): number {
|
|
350
|
+
const words1 = new Set(error1.toLowerCase().split(/\s+/));
|
|
351
|
+
const words2 = new Set(error2.toLowerCase().split(/\s+/));
|
|
352
|
+
|
|
353
|
+
let matches = 0;
|
|
354
|
+
for (const word of words1) {
|
|
355
|
+
if (words2.has(word)) matches++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const total = Math.max(words1.size, words2.size);
|
|
359
|
+
return total > 0 ? Math.round((matches / total) * 100) : 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private generateReasoning(
|
|
363
|
+
error: string,
|
|
364
|
+
likelyCause: Change | null,
|
|
365
|
+
pastBugs: PastBug[],
|
|
366
|
+
keywords: string[]
|
|
367
|
+
): string {
|
|
368
|
+
const parts: string[] = [];
|
|
369
|
+
|
|
370
|
+
if (likelyCause) {
|
|
371
|
+
const hoursAgo = Math.round((Date.now() - likelyCause.timestamp.getTime()) / (1000 * 60 * 60));
|
|
372
|
+
parts.push(`Found likely cause in ${likelyCause.file} (changed ${hoursAgo}h ago)`);
|
|
373
|
+
|
|
374
|
+
// Check for specific patterns
|
|
375
|
+
const diff = likelyCause.diff.toLowerCase();
|
|
376
|
+
if (diff.includes('-') && (diff.includes('if') || diff.includes('null') || diff.includes('?.'))) {
|
|
377
|
+
parts.push('A null/undefined check may have been removed');
|
|
378
|
+
}
|
|
379
|
+
if (likelyCause.linesRemoved > likelyCause.linesAdded) {
|
|
380
|
+
parts.push('Code was removed which might have broken functionality');
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
parts.push('Could not identify a specific recent change as the cause');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (pastBugs.length > 0) {
|
|
387
|
+
parts.push(`Found ${pastBugs.length} similar bug(s) in history`);
|
|
388
|
+
if (pastBugs[0].fix) {
|
|
389
|
+
parts.push(`Previous fix: ${pastBugs[0].fix}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Add error pattern insight
|
|
394
|
+
for (const { pattern, likelyCause: cause } of ERROR_PATTERNS) {
|
|
395
|
+
if (pattern.test(error)) {
|
|
396
|
+
parts.push(`Error type suggests: ${cause}`);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return parts.join('. ');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private getSuggestedFix(pastBugs: PastBug[], _error: string): string | null {
|
|
405
|
+
if (pastBugs.length === 0) return null;
|
|
406
|
+
|
|
407
|
+
const bestMatch = pastBugs.find(bug => bug.fix || bug.fixDiff);
|
|
408
|
+
if (bestMatch) {
|
|
409
|
+
if (bestMatch.fix) return bestMatch.fix;
|
|
410
|
+
if (bestMatch.fixDiff) return `Apply similar fix:\n${bestMatch.fixDiff.slice(0, 200)}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Get bug statistics
|
|
417
|
+
getBugStats(): {
|
|
418
|
+
total: number;
|
|
419
|
+
open: number;
|
|
420
|
+
fixed: number;
|
|
421
|
+
avgTimeToFix: number;
|
|
422
|
+
} {
|
|
423
|
+
const stats = this.db.prepare(`
|
|
424
|
+
SELECT
|
|
425
|
+
COUNT(*) as total,
|
|
426
|
+
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open,
|
|
427
|
+
SUM(CASE WHEN status = 'fixed' THEN 1 ELSE 0 END) as fixed,
|
|
428
|
+
AVG(CASE WHEN status = 'fixed' THEN fixed_at - timestamp ELSE NULL END) as avg_fix_time
|
|
429
|
+
FROM bug_history
|
|
430
|
+
`).get() as {
|
|
431
|
+
total: number;
|
|
432
|
+
open: number;
|
|
433
|
+
fixed: number;
|
|
434
|
+
avg_fix_time: number | null;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
total: stats.total,
|
|
439
|
+
open: stats.open,
|
|
440
|
+
fixed: stats.fixed,
|
|
441
|
+
avgTimeToFix: stats.avg_fix_time ? Math.round(stats.avg_fix_time / 3600) : 0 // hours
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Tier2Storage } from '../../storage/tier2.js';
|
|
3
|
+
import type { EmbeddingGenerator } from '../../indexing/embeddings.js';
|
|
4
|
+
import type {
|
|
5
|
+
Change,
|
|
6
|
+
ChangeQueryResult,
|
|
7
|
+
ChangeQueryOptions,
|
|
8
|
+
Diagnosis,
|
|
9
|
+
PastBug,
|
|
10
|
+
FixSuggestion,
|
|
11
|
+
Bug
|
|
12
|
+
} from '../../types/documentation.js';
|
|
13
|
+
import { ChangeTracker } from './change-tracker.js';
|
|
14
|
+
import { BugCorrelator } from './bug-correlator.js';
|
|
15
|
+
import { FixSuggester } from './fix-suggester.js';
|
|
16
|
+
|
|
17
|
+
export class ChangeIntelligence {
|
|
18
|
+
private changeTracker: ChangeTracker;
|
|
19
|
+
private bugCorrelator: BugCorrelator;
|
|
20
|
+
private fixSuggester: FixSuggester;
|
|
21
|
+
private initialized = false;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
projectPath: string,
|
|
25
|
+
db: Database.Database,
|
|
26
|
+
tier2: Tier2Storage,
|
|
27
|
+
embeddingGenerator: EmbeddingGenerator
|
|
28
|
+
) {
|
|
29
|
+
this.changeTracker = new ChangeTracker(projectPath, db);
|
|
30
|
+
this.bugCorrelator = new BugCorrelator(db, this.changeTracker, tier2, embeddingGenerator);
|
|
31
|
+
this.fixSuggester = new FixSuggester(db, this.bugCorrelator, this.changeTracker);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Initialize by syncing git history
|
|
35
|
+
initialize(): number {
|
|
36
|
+
if (this.initialized) return 0;
|
|
37
|
+
|
|
38
|
+
const synced = this.changeTracker.syncFromGit(100);
|
|
39
|
+
this.initialized = true;
|
|
40
|
+
return synced;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Query what changed
|
|
44
|
+
whatChanged(options: ChangeQueryOptions = {}): ChangeQueryResult {
|
|
45
|
+
return this.changeTracker.queryChanges(options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get changes for a specific file
|
|
49
|
+
whatChangedIn(file: string, limit?: number): Change[] {
|
|
50
|
+
return this.changeTracker.getFileChanges(file, limit);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get recent changes
|
|
54
|
+
getRecentChanges(hours: number = 24): Change[] {
|
|
55
|
+
return this.changeTracker.getRecentChanges(hours);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Diagnose why something broke
|
|
59
|
+
whyBroke(error: string, options?: { file?: string; line?: number }): Diagnosis {
|
|
60
|
+
return this.bugCorrelator.diagnoseBug(error, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Find similar bugs from history
|
|
64
|
+
findSimilarBugs(error: string, limit?: number): PastBug[] {
|
|
65
|
+
return this.bugCorrelator.findSimilarBugs(error, limit);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Suggest fixes for an error
|
|
69
|
+
suggestFix(error: string, context?: string): FixSuggestion[] {
|
|
70
|
+
return this.fixSuggester.suggestFix(error, context);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Record a bug for future reference
|
|
74
|
+
recordBug(error: string, options?: {
|
|
75
|
+
stackTrace?: string;
|
|
76
|
+
file?: string;
|
|
77
|
+
line?: number;
|
|
78
|
+
relatedChanges?: string[];
|
|
79
|
+
}): Bug {
|
|
80
|
+
return this.bugCorrelator.recordBug(error, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Record that a bug was fixed
|
|
84
|
+
recordFix(bugId: string, fixDiff: string, cause?: string): boolean {
|
|
85
|
+
return this.bugCorrelator.recordFix(bugId, fixDiff, cause);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Search changes by keyword
|
|
89
|
+
searchChanges(keyword: string, limit?: number): Change[] {
|
|
90
|
+
return this.changeTracker.searchChanges(keyword, limit);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get statistics
|
|
94
|
+
getStats(): {
|
|
95
|
+
changes: {
|
|
96
|
+
total: number;
|
|
97
|
+
last24h: number;
|
|
98
|
+
lastWeek: number;
|
|
99
|
+
};
|
|
100
|
+
bugs: {
|
|
101
|
+
total: number;
|
|
102
|
+
open: number;
|
|
103
|
+
fixed: number;
|
|
104
|
+
avgTimeToFix: number;
|
|
105
|
+
};
|
|
106
|
+
} {
|
|
107
|
+
const last24h = this.changeTracker.getRecentChanges(24);
|
|
108
|
+
const lastWeek = this.changeTracker.queryChanges({ since: 'last week' });
|
|
109
|
+
const bugStats = this.bugCorrelator.getBugStats();
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
changes: {
|
|
113
|
+
total: lastWeek.changes.length,
|
|
114
|
+
last24h: last24h.length,
|
|
115
|
+
lastWeek: lastWeek.changes.length
|
|
116
|
+
},
|
|
117
|
+
bugs: bugStats
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Format diagnosis for display
|
|
122
|
+
static formatDiagnosis(diagnosis: Diagnosis): string {
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
|
|
125
|
+
lines.push('\u{1F50D} Bug Diagnosis\n');
|
|
126
|
+
|
|
127
|
+
if (diagnosis.likelyCause) {
|
|
128
|
+
const change = diagnosis.likelyCause;
|
|
129
|
+
const hoursAgo = Math.round((Date.now() - change.timestamp.getTime()) / (1000 * 60 * 60));
|
|
130
|
+
|
|
131
|
+
lines.push(`\u{1F4CD} Likely Cause (${diagnosis.confidence}% confidence)`);
|
|
132
|
+
lines.push(`File: ${change.file}`);
|
|
133
|
+
lines.push(`Changed: ${hoursAgo}h ago by ${change.author}`);
|
|
134
|
+
lines.push(`Commit: ${change.commitMessage.slice(0, 50)}`);
|
|
135
|
+
|
|
136
|
+
if (change.diff) {
|
|
137
|
+
lines.push('\nDiff:');
|
|
138
|
+
lines.push(change.diff.slice(0, 300));
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
lines.push('\u{2139}\u{FE0F} Could not identify a specific cause');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (diagnosis.pastSimilarBugs.length > 0) {
|
|
145
|
+
lines.push('\n\u{1F4A1} Similar Bugs Found');
|
|
146
|
+
for (const bug of diagnosis.pastSimilarBugs.slice(0, 2)) {
|
|
147
|
+
lines.push(`- ${bug.error.slice(0, 50)} (${bug.similarity}% similar)`);
|
|
148
|
+
if (bug.fix) {
|
|
149
|
+
lines.push(` Fix: ${bug.fix}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (diagnosis.suggestedFix) {
|
|
155
|
+
lines.push('\n\u{1F527} Suggested Fix');
|
|
156
|
+
lines.push(diagnosis.suggestedFix);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push('\n' + diagnosis.reasoning);
|
|
160
|
+
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Format changes for display
|
|
165
|
+
static formatChanges(result: ChangeQueryResult): string {
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
|
|
168
|
+
lines.push(`\u{1F4CB} Changes: ${result.period}\n`);
|
|
169
|
+
lines.push(`Files Changed: ${result.totalFiles}`);
|
|
170
|
+
lines.push(`Lines: +${result.totalLinesAdded}, -${result.totalLinesRemoved}`);
|
|
171
|
+
lines.push('');
|
|
172
|
+
|
|
173
|
+
if (result.changes.length === 0) {
|
|
174
|
+
lines.push('No changes found in this period.');
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lines.push('Key Changes:');
|
|
179
|
+
for (const change of result.changes.slice(0, 10)) {
|
|
180
|
+
const time = change.timestamp.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
181
|
+
lines.push(`\u251C\u2500\u2500 ${change.file} (${time})`);
|
|
182
|
+
lines.push(`\u2502 ${change.commitMessage.slice(0, 50)}`);
|
|
183
|
+
lines.push(`\u2502 +${change.linesAdded} lines, -${change.linesRemoved} lines`);
|
|
184
|
+
lines.push(`\u2502 Author: ${change.author}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (result.changes.length > 10) {
|
|
188
|
+
lines.push(`\u2514\u2500\u2500 ${result.changes.length - 10} more changes...`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Format fix suggestions for display
|
|
195
|
+
static formatFixSuggestions(suggestions: FixSuggestion[]): string {
|
|
196
|
+
if (suggestions.length === 0) {
|
|
197
|
+
return 'No fix suggestions available.';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
lines.push('\u{1F527} Fix Suggestions\n');
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
204
|
+
const s = suggestions[i];
|
|
205
|
+
const icon = s.confidence >= 80 ? '\u{1F7E2}' :
|
|
206
|
+
s.confidence >= 60 ? '\u{1F7E1}' : '\u{1F7E0}';
|
|
207
|
+
|
|
208
|
+
lines.push(`${i + 1}. ${icon} ${s.fix} (${s.confidence}% confidence)`);
|
|
209
|
+
lines.push(` Reason: ${s.reason}`);
|
|
210
|
+
if (s.diff) {
|
|
211
|
+
lines.push(` ${s.diff}`);
|
|
212
|
+
}
|
|
213
|
+
if (s.pastFix) {
|
|
214
|
+
lines.push(` Based on fix from ${s.pastFix.date.toLocaleDateString()}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
}
|