@timmeck/brain 1.2.0 → 1.8.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 +225 -50
- package/dist/api/server.d.ts +19 -0
- package/dist/api/server.js +281 -0
- package/dist/api/server.js.map +1 -0
- package/dist/brain.d.ts +3 -0
- package/dist/brain.js +45 -8
- package/dist/brain.js.map +1 -1
- package/dist/cli/commands/dashboard.js +2 -0
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/explain.d.ts +2 -0
- package/dist/cli/commands/explain.js +76 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/code/analyzer.d.ts +6 -0
- package/dist/code/analyzer.js +35 -0
- package/dist/code/analyzer.js.map +1 -1
- package/dist/code/matcher.d.ts +11 -1
- package/dist/code/matcher.js +49 -0
- package/dist/code/matcher.js.map +1 -1
- package/dist/code/scorer.d.ts +1 -0
- package/dist/code/scorer.js +15 -1
- package/dist/code/scorer.js.map +1 -1
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.js +124 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db/migrations/007_feedback.d.ts +2 -0
- package/dist/db/migrations/007_feedback.js +12 -0
- package/dist/db/migrations/007_feedback.js.map +1 -0
- package/dist/db/migrations/008_git_integration.d.ts +2 -0
- package/dist/db/migrations/008_git_integration.js +37 -0
- package/dist/db/migrations/008_git_integration.js.map +1 -0
- package/dist/db/migrations/009_embeddings.d.ts +2 -0
- package/dist/db/migrations/009_embeddings.js +7 -0
- package/dist/db/migrations/009_embeddings.js.map +1 -0
- package/dist/db/migrations/index.js +6 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/repositories/code-module.repository.d.ts +16 -0
- package/dist/db/repositories/code-module.repository.js +42 -0
- package/dist/db/repositories/code-module.repository.js.map +1 -1
- package/dist/db/repositories/error.repository.d.ts +5 -0
- package/dist/db/repositories/error.repository.js +27 -0
- package/dist/db/repositories/error.repository.js.map +1 -1
- package/dist/db/repositories/insight.repository.d.ts +2 -0
- package/dist/db/repositories/insight.repository.js +13 -0
- package/dist/db/repositories/insight.repository.js.map +1 -1
- package/dist/embeddings/engine.d.ts +42 -0
- package/dist/embeddings/engine.js +166 -0
- package/dist/embeddings/engine.js.map +1 -0
- package/dist/hooks/post-tool-use.js +2 -0
- package/dist/hooks/post-tool-use.js.map +1 -1
- package/dist/hooks/post-write.js +11 -0
- package/dist/hooks/post-write.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc/router.d.ts +2 -0
- package/dist/ipc/router.js +13 -0
- package/dist/ipc/router.js.map +1 -1
- package/dist/learning/confidence-scorer.d.ts +16 -0
- package/dist/learning/confidence-scorer.js +20 -0
- package/dist/learning/confidence-scorer.js.map +1 -1
- package/dist/learning/learning-engine.js +12 -5
- package/dist/learning/learning-engine.js.map +1 -1
- package/dist/matching/error-matcher.d.ts +9 -1
- package/dist/matching/error-matcher.js +50 -5
- package/dist/matching/error-matcher.js.map +1 -1
- package/dist/mcp/http-server.d.ts +14 -0
- package/dist/mcp/http-server.js +117 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/tools.d.ts +4 -0
- package/dist/mcp/tools.js +41 -14
- package/dist/mcp/tools.js.map +1 -1
- package/dist/services/analytics.service.d.ts +39 -0
- package/dist/services/analytics.service.js +111 -0
- package/dist/services/analytics.service.js.map +1 -1
- package/dist/services/code.service.d.ts +2 -0
- package/dist/services/code.service.js +62 -4
- package/dist/services/code.service.js.map +1 -1
- package/dist/services/error.service.d.ts +17 -1
- package/dist/services/error.service.js +90 -12
- package/dist/services/error.service.js.map +1 -1
- package/dist/services/git.service.d.ts +49 -0
- package/dist/services/git.service.js +112 -0
- package/dist/services/git.service.js.map +1 -0
- package/dist/services/prevention.service.d.ts +7 -0
- package/dist/services/prevention.service.js +38 -0
- package/dist/services/prevention.service.js.map +1 -1
- package/dist/services/research.service.d.ts +1 -0
- package/dist/services/research.service.js +4 -0
- package/dist/services/research.service.js.map +1 -1
- package/dist/services/solution.service.d.ts +10 -0
- package/dist/services/solution.service.js +48 -0
- package/dist/services/solution.service.js.map +1 -1
- package/dist/types/config.types.d.ts +21 -0
- package/dist/types/synapse.types.d.ts +1 -1
- package/package.json +8 -3
- package/src/api/server.ts +321 -0
- package/src/brain.ts +50 -8
- package/src/cli/commands/dashboard.ts +2 -0
- package/src/cli/commands/explain.ts +83 -0
- package/src/code/analyzer.ts +40 -0
- package/src/code/matcher.ts +67 -2
- package/src/code/scorer.ts +13 -1
- package/src/config.ts +24 -0
- package/src/dashboard/server.ts +142 -0
- package/src/db/migrations/007_feedback.ts +13 -0
- package/src/db/migrations/008_git_integration.ts +38 -0
- package/src/db/migrations/009_embeddings.ts +8 -0
- package/src/db/migrations/index.ts +6 -0
- package/src/db/repositories/code-module.repository.ts +53 -0
- package/src/db/repositories/error.repository.ts +40 -0
- package/src/db/repositories/insight.repository.ts +21 -0
- package/src/embeddings/engine.ts +217 -0
- package/src/hooks/post-tool-use.ts +2 -0
- package/src/hooks/post-write.ts +12 -0
- package/src/index.ts +3 -1
- package/src/ipc/router.ts +16 -0
- package/src/learning/confidence-scorer.ts +33 -0
- package/src/learning/learning-engine.ts +13 -5
- package/src/matching/error-matcher.ts +55 -4
- package/src/mcp/http-server.ts +137 -0
- package/src/mcp/tools.ts +52 -14
- package/src/services/analytics.service.ts +136 -0
- package/src/services/code.service.ts +87 -4
- package/src/services/error.service.ts +114 -13
- package/src/services/git.service.ts +132 -0
- package/src/services/prevention.service.ts +40 -0
- package/src/services/research.service.ts +5 -0
- package/src/services/solution.service.ts +58 -0
- package/src/types/config.types.ts +24 -0
- package/src/types/synapse.types.ts +1 -0
|
@@ -2,6 +2,8 @@ import type { ErrorRecord } from '../types/error.types.js';
|
|
|
2
2
|
import type { ErrorRepository } from '../db/repositories/error.repository.js';
|
|
3
3
|
import type { ProjectRepository } from '../db/repositories/project.repository.js';
|
|
4
4
|
import type { SynapseManager } from '../synapses/synapse-manager.js';
|
|
5
|
+
import type { MatchingConfig } from '../types/config.types.js';
|
|
6
|
+
import type { EmbeddingEngine } from '../embeddings/engine.js';
|
|
5
7
|
import { parseError } from '../parsing/error-parser.js';
|
|
6
8
|
import { generateFingerprint } from '../matching/fingerprint.js';
|
|
7
9
|
import { matchError, type MatchResult } from '../matching/error-matcher.js';
|
|
@@ -13,6 +15,9 @@ export interface ReportErrorInput {
|
|
|
13
15
|
errorOutput: string;
|
|
14
16
|
filePath?: string;
|
|
15
17
|
terminalId?: number;
|
|
18
|
+
taskContext?: string;
|
|
19
|
+
workingDirectory?: string;
|
|
20
|
+
command?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export interface ErrorQueryInput {
|
|
@@ -25,14 +30,23 @@ export interface ErrorQueryInput {
|
|
|
25
30
|
export class ErrorService {
|
|
26
31
|
private logger = getLogger();
|
|
27
32
|
private eventBus = getEventBus();
|
|
33
|
+
private matchingConfig: MatchingConfig | null = null;
|
|
34
|
+
private embeddingEngine: EmbeddingEngine | null = null;
|
|
28
35
|
|
|
29
36
|
constructor(
|
|
30
37
|
private errorRepo: ErrorRepository,
|
|
31
38
|
private projectRepo: ProjectRepository,
|
|
32
39
|
private synapseManager: SynapseManager,
|
|
33
|
-
|
|
40
|
+
matchingConfig?: MatchingConfig,
|
|
41
|
+
) {
|
|
42
|
+
this.matchingConfig = matchingConfig ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setEmbeddingEngine(engine: EmbeddingEngine): void {
|
|
46
|
+
this.embeddingEngine = engine;
|
|
47
|
+
}
|
|
34
48
|
|
|
35
|
-
report(input: ReportErrorInput): { errorId: number; isNew: boolean; matches: MatchResult[] } {
|
|
49
|
+
report(input: ReportErrorInput): { errorId: number; isNew: boolean; matches: MatchResult[]; crossProjectMatches?: MatchResult[] } {
|
|
36
50
|
// 1. Ensure project exists
|
|
37
51
|
let project = this.projectRepo.findByName(input.project);
|
|
38
52
|
if (!project) {
|
|
@@ -40,7 +54,10 @@ export class ErrorService {
|
|
|
40
54
|
project = this.projectRepo.getById(id)!;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
// 2.
|
|
57
|
+
// 2. Build context from available information
|
|
58
|
+
const context = this.buildContext(input);
|
|
59
|
+
|
|
60
|
+
// 3. Parse the error
|
|
44
61
|
const parsed = parseError(input.errorOutput);
|
|
45
62
|
if (!parsed) {
|
|
46
63
|
this.logger.warn('Could not parse error output');
|
|
@@ -51,7 +68,7 @@ export class ErrorService {
|
|
|
51
68
|
type: 'UnknownError',
|
|
52
69
|
message: input.errorOutput.split('\n')[0] ?? input.errorOutput,
|
|
53
70
|
raw_output: input.errorOutput,
|
|
54
|
-
context
|
|
71
|
+
context,
|
|
55
72
|
file_path: input.filePath ?? null,
|
|
56
73
|
line_number: null,
|
|
57
74
|
column_number: null,
|
|
@@ -59,14 +76,18 @@ export class ErrorService {
|
|
|
59
76
|
return { errorId, isNew: true, matches: [] };
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
//
|
|
79
|
+
// 4. Generate fingerprint
|
|
63
80
|
const fingerprint = generateFingerprint(parsed.errorType, parsed.message, parsed.frames);
|
|
64
81
|
|
|
65
|
-
//
|
|
82
|
+
// 5. Check for existing error with same fingerprint
|
|
66
83
|
const existing = this.errorRepo.findByFingerprint(fingerprint);
|
|
67
84
|
if (existing.length > 0) {
|
|
68
85
|
const err = existing[0]!;
|
|
69
86
|
this.errorRepo.incrementOccurrence(err.id);
|
|
87
|
+
// Update context if previously null
|
|
88
|
+
if (!err.context && context) {
|
|
89
|
+
this.errorRepo.update(err.id, { context });
|
|
90
|
+
}
|
|
70
91
|
this.logger.info(`Known error (id=${err.id}), occurrence incremented`);
|
|
71
92
|
|
|
72
93
|
// Strengthen synapse
|
|
@@ -79,7 +100,7 @@ export class ErrorService {
|
|
|
79
100
|
return { errorId: err.id, isNew: false, matches: [] };
|
|
80
101
|
}
|
|
81
102
|
|
|
82
|
-
//
|
|
103
|
+
// 6. Create new error record
|
|
83
104
|
const errorId = this.errorRepo.create({
|
|
84
105
|
project_id: project.id,
|
|
85
106
|
terminal_id: input.terminalId ?? null,
|
|
@@ -87,26 +108,26 @@ export class ErrorService {
|
|
|
87
108
|
type: parsed.errorType,
|
|
88
109
|
message: parsed.message,
|
|
89
110
|
raw_output: input.errorOutput,
|
|
90
|
-
context
|
|
111
|
+
context,
|
|
91
112
|
file_path: parsed.sourceFile ?? input.filePath ?? null,
|
|
92
113
|
line_number: parsed.sourceLine ?? null,
|
|
93
114
|
column_number: null,
|
|
94
115
|
});
|
|
95
116
|
|
|
96
|
-
//
|
|
117
|
+
// 7. Create synapse: error ↔ project
|
|
97
118
|
this.synapseManager.strengthen(
|
|
98
119
|
{ type: 'error', id: errorId },
|
|
99
120
|
{ type: 'project', id: project.id },
|
|
100
121
|
'co_occurs',
|
|
101
122
|
);
|
|
102
123
|
|
|
103
|
-
//
|
|
124
|
+
// 8. Find similar errors
|
|
104
125
|
const candidates = this.errorRepo.findByProject(project.id)
|
|
105
126
|
.filter(e => e.id !== errorId);
|
|
106
127
|
const newError = this.errorRepo.getById(errorId)!;
|
|
107
128
|
const matches = matchError(newError, candidates);
|
|
108
129
|
|
|
109
|
-
//
|
|
130
|
+
// 9. Create similarity synapses for strong matches
|
|
110
131
|
for (const match of matches.filter(m => m.isStrong)) {
|
|
111
132
|
this.synapseManager.strengthen(
|
|
112
133
|
{ type: 'error', id: errorId },
|
|
@@ -115,10 +136,26 @@ export class ErrorService {
|
|
|
115
136
|
);
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
// 10. Error Chain Detection: link to recent unresolved errors in same project
|
|
140
|
+
this.detectErrorChain(errorId, project.id);
|
|
141
|
+
|
|
142
|
+
// 11. Cross-Project Transfer Learning
|
|
143
|
+
let crossProjectMatches: MatchResult[] = [];
|
|
144
|
+
if (this.matchingConfig?.crossProjectMatching) {
|
|
145
|
+
crossProjectMatches = this.findCrossProjectMatches(newError, project.id);
|
|
146
|
+
for (const match of crossProjectMatches.filter(m => m.isStrong)) {
|
|
147
|
+
this.synapseManager.strengthen(
|
|
148
|
+
{ type: 'error', id: errorId },
|
|
149
|
+
{ type: 'error', id: match.errorId },
|
|
150
|
+
'cross_project',
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
this.eventBus.emit('error:reported', { errorId, projectId: project.id, fingerprint });
|
|
119
156
|
this.logger.info(`New error reported (id=${errorId}, type=${parsed.errorType})`);
|
|
120
157
|
|
|
121
|
-
return { errorId, isNew: true, matches };
|
|
158
|
+
return { errorId, isNew: true, matches, crossProjectMatches };
|
|
122
159
|
}
|
|
123
160
|
|
|
124
161
|
query(input: ErrorQueryInput): ErrorRecord[] {
|
|
@@ -140,7 +177,13 @@ export class ErrorService {
|
|
|
140
177
|
|
|
141
178
|
const candidates = this.errorRepo.findByProject(error.project_id)
|
|
142
179
|
.filter(e => e.id !== errorId);
|
|
143
|
-
|
|
180
|
+
|
|
181
|
+
// Hybrid search: include vector scores if embedding engine is available
|
|
182
|
+
const vectorScores = this.embeddingEngine?.isReady()
|
|
183
|
+
? this.embeddingEngine.computeErrorVectorScores(errorId, error.project_id)
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
186
|
+
return matchError(error, candidates, vectorScores);
|
|
144
187
|
}
|
|
145
188
|
|
|
146
189
|
resolve(errorId: number, solutionId?: number): void {
|
|
@@ -161,4 +204,62 @@ export class ErrorService {
|
|
|
161
204
|
countSince(since: string, projectId?: number): number {
|
|
162
205
|
return this.errorRepo.countSince(since, projectId);
|
|
163
206
|
}
|
|
207
|
+
|
|
208
|
+
getErrorChain(errorId: number): { parents: ErrorRecord[]; children: ErrorRecord[] } {
|
|
209
|
+
return {
|
|
210
|
+
parents: this.errorRepo.findChainParents(errorId),
|
|
211
|
+
children: this.errorRepo.findChainChildren(errorId),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private findCrossProjectMatches(error: ErrorRecord, currentProjectId: number): MatchResult[] {
|
|
216
|
+
const allProjects = this.projectRepo.getAll();
|
|
217
|
+
const otherProjects = allProjects.filter(p => p.id !== currentProjectId);
|
|
218
|
+
const weight = this.matchingConfig?.crossProjectWeight ?? 0.7;
|
|
219
|
+
|
|
220
|
+
const allMatches: MatchResult[] = [];
|
|
221
|
+
for (const project of otherProjects) {
|
|
222
|
+
const candidates = this.errorRepo.findByProject(project.id)
|
|
223
|
+
.filter(e => e.resolved === 1); // Only look at resolved errors from other projects
|
|
224
|
+
const matches = matchError(error, candidates);
|
|
225
|
+
// Apply cross-project weight discount
|
|
226
|
+
for (const match of matches) {
|
|
227
|
+
match.score *= weight;
|
|
228
|
+
match.isStrong = match.score >= 0.90;
|
|
229
|
+
allMatches.push(match);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return allMatches.sort((a, b) => b.score - a.score).slice(0, 5);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private detectErrorChain(newErrorId: number, projectId: number): void {
|
|
237
|
+
// Look for recent unresolved errors in the same project (last 10 minutes)
|
|
238
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
|
239
|
+
const recentErrors = this.errorRepo.findRecentByProject(projectId, tenMinutesAgo, 5);
|
|
240
|
+
|
|
241
|
+
for (const recent of recentErrors) {
|
|
242
|
+
if (recent.id === newErrorId) continue;
|
|
243
|
+
if (recent.resolved === 0) {
|
|
244
|
+
// This new error appeared while trying to fix a recent error → chain
|
|
245
|
+
this.errorRepo.createChain(recent.id, newErrorId, 'caused_by_fix');
|
|
246
|
+
this.synapseManager.strengthen(
|
|
247
|
+
{ type: 'error', id: recent.id },
|
|
248
|
+
{ type: 'error', id: newErrorId },
|
|
249
|
+
'causes',
|
|
250
|
+
);
|
|
251
|
+
this.logger.info(`Error chain: #${recent.id} → #${newErrorId} (caused_by_fix)`);
|
|
252
|
+
break; // Link to most recent parent only
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private buildContext(input: ReportErrorInput): string | null {
|
|
258
|
+
const parts: string[] = [];
|
|
259
|
+
if (input.taskContext) parts.push(`task: ${input.taskContext}`);
|
|
260
|
+
if (input.command) parts.push(`command: ${input.command}`);
|
|
261
|
+
if (input.workingDirectory) parts.push(`cwd: ${input.workingDirectory}`);
|
|
262
|
+
if (input.filePath) parts.push(`file: ${input.filePath}`);
|
|
263
|
+
return parts.length > 0 ? parts.join(' | ') : null;
|
|
264
|
+
}
|
|
164
265
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { SynapseManager } from '../synapses/synapse-manager.js';
|
|
4
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export interface GitCommitInfo {
|
|
7
|
+
hash: string;
|
|
8
|
+
message: string;
|
|
9
|
+
author: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
filesChanged: number;
|
|
12
|
+
insertions: number;
|
|
13
|
+
deletions: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class GitService {
|
|
17
|
+
private logger = getLogger();
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private db: Database.Database,
|
|
21
|
+
private synapseManager: SynapseManager,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get current git info for context enrichment
|
|
26
|
+
*/
|
|
27
|
+
getGitContext(cwd?: string): { branch: string | null; diff: string | null; lastCommit: string | null } {
|
|
28
|
+
try {
|
|
29
|
+
const opts = cwd ? { cwd, timeout: 5000 } : { timeout: 5000 };
|
|
30
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { ...opts, encoding: 'utf8' }).trim();
|
|
31
|
+
const diff = execSync('git diff --stat HEAD', { ...opts, encoding: 'utf8' }).trim();
|
|
32
|
+
const lastCommit = execSync('git log -1 --pretty=format:"%H %s"', { ...opts, encoding: 'utf8' }).trim();
|
|
33
|
+
return { branch, diff: diff || null, lastCommit };
|
|
34
|
+
} catch {
|
|
35
|
+
return { branch: null, diff: null, lastCommit: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Store a git commit and link it to an error
|
|
41
|
+
*/
|
|
42
|
+
linkErrorToCommit(errorId: number, projectId: number, commitHash: string, relationship: string = 'introduced_by'): void {
|
|
43
|
+
try {
|
|
44
|
+
// Store commit info
|
|
45
|
+
const commitInfo = this.getCommitInfo(commitHash);
|
|
46
|
+
if (commitInfo) {
|
|
47
|
+
this.db.prepare(`
|
|
48
|
+
INSERT OR IGNORE INTO git_commits (project_id, commit_hash, message, author, timestamp, files_changed, insertions, deletions)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
50
|
+
`).run(projectId, commitInfo.hash, commitInfo.message, commitInfo.author, commitInfo.timestamp,
|
|
51
|
+
commitInfo.filesChanged, commitInfo.insertions, commitInfo.deletions);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Link error to commit
|
|
55
|
+
this.db.prepare(`
|
|
56
|
+
INSERT OR IGNORE INTO error_commits (error_id, commit_hash, relationship)
|
|
57
|
+
VALUES (?, ?, ?)
|
|
58
|
+
`).run(errorId, commitHash, relationship);
|
|
59
|
+
|
|
60
|
+
this.logger.info(`Linked error #${errorId} to commit ${commitHash.slice(0, 8)} (${relationship})`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
this.logger.warn(`Failed to link error to commit: ${err}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find which commit introduced an error
|
|
68
|
+
*/
|
|
69
|
+
findIntroducingCommit(errorId: number): Array<{ commitHash: string; message: string; relationship: string }> {
|
|
70
|
+
const rows = this.db.prepare(`
|
|
71
|
+
SELECT ec.commit_hash, ec.relationship, gc.message
|
|
72
|
+
FROM error_commits ec
|
|
73
|
+
LEFT JOIN git_commits gc ON ec.commit_hash = gc.commit_hash
|
|
74
|
+
WHERE ec.error_id = ?
|
|
75
|
+
ORDER BY ec.created_at DESC
|
|
76
|
+
`).all(errorId) as Array<{ commit_hash: string; message: string | null; relationship: string }>;
|
|
77
|
+
|
|
78
|
+
return rows.map(r => ({
|
|
79
|
+
commitHash: r.commit_hash,
|
|
80
|
+
message: r.message ?? 'unknown',
|
|
81
|
+
relationship: r.relationship,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Find errors introduced by a specific commit
|
|
87
|
+
*/
|
|
88
|
+
findErrorsByCommit(commitHash: string): Array<{ errorId: number; relationship: string }> {
|
|
89
|
+
const rows = this.db.prepare(`
|
|
90
|
+
SELECT error_id, relationship FROM error_commits WHERE commit_hash = ?
|
|
91
|
+
`).all(commitHash) as Array<{ error_id: number; relationship: string }>;
|
|
92
|
+
|
|
93
|
+
return rows.map(r => ({ errorId: r.error_id, relationship: r.relationship }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Capture current git diff for error context
|
|
98
|
+
*/
|
|
99
|
+
captureDiff(cwd?: string): string | null {
|
|
100
|
+
try {
|
|
101
|
+
const opts = cwd ? { cwd, timeout: 5000, encoding: 'utf8' as const } : { timeout: 5000, encoding: 'utf8' as const };
|
|
102
|
+
const diff = execSync('git diff HEAD --no-color', opts).trim();
|
|
103
|
+
// Truncate to avoid huge diffs
|
|
104
|
+
return diff.length > 5000 ? diff.slice(0, 5000) + '\n... (truncated)' : diff || null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private getCommitInfo(hash: string, cwd?: string): GitCommitInfo | null {
|
|
111
|
+
try {
|
|
112
|
+
const opts = cwd ? { cwd, timeout: 5000, encoding: 'utf8' as const } : { timeout: 5000, encoding: 'utf8' as const };
|
|
113
|
+
const info = execSync(`git log -1 --pretty=format:"%H|||%s|||%an|||%aI" ${hash}`, opts).trim();
|
|
114
|
+
const [commitHash, message, author, timestamp] = info.split('|||');
|
|
115
|
+
|
|
116
|
+
const stat = execSync(`git diff --stat ${hash}~1..${hash}`, opts).trim();
|
|
117
|
+
const statMatch = stat.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
hash: commitHash ?? hash,
|
|
121
|
+
message: message ?? '',
|
|
122
|
+
author: author ?? '',
|
|
123
|
+
timestamp: timestamp ?? new Date().toISOString(),
|
|
124
|
+
filesChanged: parseInt(statMatch?.[1] ?? '0', 10),
|
|
125
|
+
insertions: parseInt(statMatch?.[2] ?? '0', 10),
|
|
126
|
+
deletions: parseInt(statMatch?.[3] ?? '0', 10),
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -105,6 +105,46 @@ export class PreventionService {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
checkCodeForPatterns(source: string, filePath?: string): { warnings: Array<{ message: string; severity: string; ruleId?: number }> } {
|
|
109
|
+
const warnings: Array<{ message: string; severity: string; ruleId?: number }> = [];
|
|
110
|
+
|
|
111
|
+
// Check antipatterns against the code itself
|
|
112
|
+
const globalAntipatterns = this.antipatternRepo.findGlobal();
|
|
113
|
+
for (const ap of globalAntipatterns) {
|
|
114
|
+
try {
|
|
115
|
+
const pattern = new RegExp(ap.pattern, 'i');
|
|
116
|
+
if (pattern.test(source)) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
message: `Code matches known error pattern: ${ap.description}${ap.suggestion ? `. Suggestion: ${ap.suggestion}` : ''}`,
|
|
119
|
+
severity: ap.severity,
|
|
120
|
+
ruleId: undefined,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Invalid regex, skip
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check active rules
|
|
129
|
+
const rules = this.ruleRepo.findActive();
|
|
130
|
+
for (const rule of rules) {
|
|
131
|
+
try {
|
|
132
|
+
const pattern = new RegExp(rule.pattern, 'i');
|
|
133
|
+
if (pattern.test(source)) {
|
|
134
|
+
warnings.push({
|
|
135
|
+
message: `Code matches learned rule: ${rule.description ?? rule.pattern}. Action: ${rule.action}`,
|
|
136
|
+
severity: 'warning',
|
|
137
|
+
ruleId: rule.id,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Invalid regex, skip
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { warnings: warnings.slice(0, 5) };
|
|
146
|
+
}
|
|
147
|
+
|
|
108
148
|
reportPrevention(ruleId: number, errorId: number): void {
|
|
109
149
|
const rule = this.ruleRepo.getById(ruleId);
|
|
110
150
|
if (rule) {
|
|
@@ -90,4 +90,9 @@ export class ResearchService {
|
|
|
90
90
|
expireOldInsights(): number {
|
|
91
91
|
return this.insightRepo.expire();
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
rateInsight(id: number, rating: number, comment?: string): boolean {
|
|
95
|
+
const clamped = Math.max(-1, Math.min(1, rating)); // -1 (bad), 0 (neutral), 1 (useful)
|
|
96
|
+
return this.insightRepo.rate(id, clamped, comment);
|
|
97
|
+
}
|
|
93
98
|
}
|
|
@@ -113,4 +113,62 @@ export class SolutionService {
|
|
|
113
113
|
successRate(solutionId: number): number {
|
|
114
114
|
return this.solutionRepo.successRate(solutionId);
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
analyzeEfficiency(): {
|
|
118
|
+
avgDurationMs: number;
|
|
119
|
+
slowSolutions: Array<{ solutionId: number; avgDuration: number; description: string }>;
|
|
120
|
+
successRateOverall: number;
|
|
121
|
+
totalAttempts: number;
|
|
122
|
+
} {
|
|
123
|
+
const allSolutions = this.solutionRepo.getAll();
|
|
124
|
+
let totalDuration = 0;
|
|
125
|
+
let totalAttempts = 0;
|
|
126
|
+
let totalSuccessRate = 0;
|
|
127
|
+
let solutionCount = 0;
|
|
128
|
+
const solutionDurations: Array<{ solutionId: number; avgDuration: number; description: string }> = [];
|
|
129
|
+
|
|
130
|
+
for (const solution of allSolutions) {
|
|
131
|
+
const rate = this.solutionRepo.successRate(solution.id);
|
|
132
|
+
if (solution.success_count + solution.fail_count > 0) {
|
|
133
|
+
totalSuccessRate += rate;
|
|
134
|
+
solutionCount++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check attempts for duration data
|
|
138
|
+
const attempts = this.solutionRepo.getAttempts(solution.id);
|
|
139
|
+
if (attempts.length > 0) {
|
|
140
|
+
let solDuration = 0;
|
|
141
|
+
let solAttemptCount = 0;
|
|
142
|
+
for (const attempt of attempts) {
|
|
143
|
+
if (attempt.duration_ms && attempt.duration_ms > 0) {
|
|
144
|
+
solDuration += attempt.duration_ms;
|
|
145
|
+
solAttemptCount++;
|
|
146
|
+
totalDuration += attempt.duration_ms;
|
|
147
|
+
totalAttempts++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (solAttemptCount > 0) {
|
|
151
|
+
solutionDurations.push({
|
|
152
|
+
solutionId: solution.id,
|
|
153
|
+
avgDuration: solDuration / solAttemptCount,
|
|
154
|
+
description: solution.description,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find slow solutions (above 2x average)
|
|
161
|
+
const avgDurationMs = totalAttempts > 0 ? totalDuration / totalAttempts : 0;
|
|
162
|
+
const slowSolutions = solutionDurations
|
|
163
|
+
.filter(s => s.avgDuration > avgDurationMs * 2)
|
|
164
|
+
.sort((a, b) => b.avgDuration - a.avgDuration)
|
|
165
|
+
.slice(0, 10);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
avgDurationMs,
|
|
169
|
+
slowSolutions,
|
|
170
|
+
successRateOverall: solutionCount > 0 ? totalSuccessRate / solutionCount : 0,
|
|
171
|
+
totalAttempts,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
116
174
|
}
|
|
@@ -22,6 +22,8 @@ export interface MatchingConfig {
|
|
|
22
22
|
fingerprintFields: string[];
|
|
23
23
|
similarityThreshold: number;
|
|
24
24
|
maxResults: number;
|
|
25
|
+
crossProjectMatching: boolean;
|
|
26
|
+
crossProjectWeight: number;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface CodeConfig {
|
|
@@ -64,10 +66,32 @@ export interface RetentionConfig {
|
|
|
64
66
|
insightDays: number;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
export interface ApiConfig {
|
|
70
|
+
port: number;
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
apiKey?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface McpHttpConfig {
|
|
76
|
+
port: number;
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EmbeddingsConfig {
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
modelName: string;
|
|
83
|
+
cacheDir: string;
|
|
84
|
+
sweepIntervalMs: number;
|
|
85
|
+
batchSize: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
export interface BrainConfig {
|
|
68
89
|
dataDir: string;
|
|
69
90
|
dbPath: string;
|
|
70
91
|
ipc: IpcConfig;
|
|
92
|
+
api: ApiConfig;
|
|
93
|
+
mcpHttp: McpHttpConfig;
|
|
94
|
+
embeddings: EmbeddingsConfig;
|
|
71
95
|
learning: LearningConfig;
|
|
72
96
|
terminal: TerminalConfig;
|
|
73
97
|
matching: MatchingConfig;
|