@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
package/src/mcp/tools.ts
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import type { IpcClient } from '../ipc/client.js';
|
|
4
|
+
import type { IpcRouter } from '../ipc/router.js';
|
|
4
5
|
|
|
5
6
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
7
|
type AnyResult = any;
|
|
7
8
|
|
|
9
|
+
type BrainCall = (method: string, params?: unknown) => Promise<unknown> | unknown;
|
|
10
|
+
|
|
8
11
|
function textResult(data: unknown): { content: Array<{ type: 'text'; text: string }> } {
|
|
9
12
|
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
10
13
|
return { content: [{ type: 'text' as const, text }] };
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
/** Register tools using IPC client (for stdio MCP transport) */
|
|
13
17
|
export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
18
|
+
registerToolsWithCaller(server, (method, params) => ipc.request(method, params));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Register tools using router directly (for HTTP MCP transport inside daemon) */
|
|
22
|
+
export function registerToolsDirect(server: McpServer, router: IpcRouter): void {
|
|
23
|
+
registerToolsWithCaller(server, (method, params) => router.handle(method, params));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function registerToolsWithCaller(server: McpServer, call: BrainCall): void {
|
|
14
27
|
|
|
15
28
|
// === Error Brain Tools ===
|
|
16
29
|
|
|
@@ -25,16 +38,23 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
25
38
|
project: z.string().optional().describe('Project name'),
|
|
26
39
|
},
|
|
27
40
|
async (params) => {
|
|
28
|
-
const result: AnyResult = await
|
|
41
|
+
const result: AnyResult = await call('error.report', {
|
|
29
42
|
project: params.project ?? 'default',
|
|
30
43
|
errorOutput: params.error_output,
|
|
31
44
|
filePath: params.working_directory,
|
|
45
|
+
taskContext: params.task_context,
|
|
46
|
+
workingDirectory: params.working_directory,
|
|
47
|
+
command: params.command,
|
|
32
48
|
});
|
|
33
49
|
let response = `Error #${result.errorId} recorded (${result.isNew ? 'new' : 'seen before'}).`;
|
|
34
50
|
if (result.matches?.length > 0) {
|
|
35
51
|
const best = result.matches[0];
|
|
36
52
|
response += `\nSimilar error found (#${best.errorId}, ${Math.round(best.score * 100)}% match).`;
|
|
37
53
|
}
|
|
54
|
+
if (result.crossProjectMatches?.length > 0) {
|
|
55
|
+
const best = result.crossProjectMatches[0];
|
|
56
|
+
response += `\nCross-project match found (#${best.errorId}, ${Math.round(best.score * 100)}% match from another project).`;
|
|
57
|
+
}
|
|
38
58
|
return textResult(response);
|
|
39
59
|
},
|
|
40
60
|
);
|
|
@@ -47,7 +67,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
47
67
|
project_only: z.boolean().optional().describe('Only search in current project'),
|
|
48
68
|
},
|
|
49
69
|
async (params) => {
|
|
50
|
-
const results: AnyResult = await
|
|
70
|
+
const results: AnyResult = await call('error.query', {
|
|
51
71
|
search: params.query,
|
|
52
72
|
});
|
|
53
73
|
if (!results?.length) return textResult('No matching errors found.');
|
|
@@ -68,7 +88,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
68
88
|
code_change: z.string().optional().describe('Code changes or diff'),
|
|
69
89
|
},
|
|
70
90
|
async (params) => {
|
|
71
|
-
const solutionId: AnyResult = await
|
|
91
|
+
const solutionId: AnyResult = await call('solution.report', {
|
|
72
92
|
errorId: params.error_id,
|
|
73
93
|
description: params.description,
|
|
74
94
|
commands: params.commands,
|
|
@@ -88,7 +108,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
88
108
|
output: z.string().optional().describe('Output of the failed attempt'),
|
|
89
109
|
},
|
|
90
110
|
async (params) => {
|
|
91
|
-
await
|
|
111
|
+
await call('solution.rate', {
|
|
92
112
|
errorId: params.error_id,
|
|
93
113
|
solutionId: params.solution_id,
|
|
94
114
|
success: false,
|
|
@@ -108,7 +128,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
108
128
|
language: z.string().optional().describe('Programming language'),
|
|
109
129
|
},
|
|
110
130
|
async (params) => {
|
|
111
|
-
const results: AnyResult = await
|
|
131
|
+
const results: AnyResult = await call('code.find', {
|
|
112
132
|
query: params.purpose,
|
|
113
133
|
language: params.language,
|
|
114
134
|
});
|
|
@@ -132,7 +152,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
132
152
|
description: z.string().optional().describe('What this code does'),
|
|
133
153
|
},
|
|
134
154
|
async (params) => {
|
|
135
|
-
const result: AnyResult = await
|
|
155
|
+
const result: AnyResult = await call('code.analyze', {
|
|
136
156
|
project: params.project ?? 'default',
|
|
137
157
|
name: params.name ?? params.file_path.split('/').pop() ?? 'unknown',
|
|
138
158
|
filePath: params.file_path,
|
|
@@ -153,7 +173,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
153
173
|
file_path: z.string().optional().describe('File path for context'),
|
|
154
174
|
},
|
|
155
175
|
async (params) => {
|
|
156
|
-
const results: AnyResult = await
|
|
176
|
+
const results: AnyResult = await call('code.similarity', {
|
|
157
177
|
source: params.source_code,
|
|
158
178
|
language: params.language ?? detectLanguage(params.file_path ?? ''),
|
|
159
179
|
});
|
|
@@ -176,7 +196,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
176
196
|
max_depth: z.number().optional().describe('How many hops to follow (default: 3)'),
|
|
177
197
|
},
|
|
178
198
|
async (params) => {
|
|
179
|
-
const context: AnyResult = await
|
|
199
|
+
const context: AnyResult = await call('synapse.context', {
|
|
180
200
|
errorId: params.node_id,
|
|
181
201
|
});
|
|
182
202
|
const sections: string[] = [];
|
|
@@ -199,7 +219,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
199
219
|
to_id: z.number().describe('Target ID'),
|
|
200
220
|
},
|
|
201
221
|
async (params) => {
|
|
202
|
-
const path: AnyResult = await
|
|
222
|
+
const path: AnyResult = await call('synapse.path', params);
|
|
203
223
|
if (!path) return textResult('No connection found between these nodes.');
|
|
204
224
|
return textResult(path);
|
|
205
225
|
},
|
|
@@ -215,7 +235,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
215
235
|
priority: z.string().optional().describe('Minimum priority: low, medium, high, critical'),
|
|
216
236
|
},
|
|
217
237
|
async (params) => {
|
|
218
|
-
const insights: AnyResult = await
|
|
238
|
+
const insights: AnyResult = await call('research.insights', {
|
|
219
239
|
type: params.type,
|
|
220
240
|
activeOnly: true,
|
|
221
241
|
limit: 20,
|
|
@@ -228,6 +248,24 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
228
248
|
},
|
|
229
249
|
);
|
|
230
250
|
|
|
251
|
+
server.tool(
|
|
252
|
+
'brain_rate_insight',
|
|
253
|
+
'Rate an insight as useful or not useful. Helps Brain learn what insights matter.',
|
|
254
|
+
{
|
|
255
|
+
insight_id: z.number().describe('The insight ID to rate'),
|
|
256
|
+
rating: z.number().describe('Rating: 1 (useful), 0 (neutral), -1 (not useful)'),
|
|
257
|
+
comment: z.string().optional().describe('Optional feedback comment'),
|
|
258
|
+
},
|
|
259
|
+
async (params) => {
|
|
260
|
+
const success: AnyResult = await call('insight.rate', {
|
|
261
|
+
id: params.insight_id,
|
|
262
|
+
rating: params.rating,
|
|
263
|
+
comment: params.comment,
|
|
264
|
+
});
|
|
265
|
+
return textResult(success ? `Insight #${params.insight_id} rated.` : `Insight #${params.insight_id} not found.`);
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
231
269
|
server.tool(
|
|
232
270
|
'brain_suggest',
|
|
233
271
|
'Ask Brain for suggestions: what to build next, what to improve, what patterns to extract.',
|
|
@@ -235,7 +273,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
235
273
|
context: z.string().describe('Current context or question'),
|
|
236
274
|
},
|
|
237
275
|
async (params) => {
|
|
238
|
-
const suggestions: AnyResult = await
|
|
276
|
+
const suggestions: AnyResult = await call('research.suggest', {
|
|
239
277
|
context: params.context,
|
|
240
278
|
});
|
|
241
279
|
return textResult(suggestions);
|
|
@@ -249,8 +287,8 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
249
287
|
'Get current Brain status: errors, solutions, code modules, synapse network, insights.',
|
|
250
288
|
{},
|
|
251
289
|
async () => {
|
|
252
|
-
const summary: AnyResult = await
|
|
253
|
-
const network: AnyResult = await
|
|
290
|
+
const summary: AnyResult = await call('analytics.summary', {});
|
|
291
|
+
const network: AnyResult = await call('synapse.stats', {});
|
|
254
292
|
const lines = [
|
|
255
293
|
`Errors: ${summary.errors?.total ?? 0} total, ${summary.errors?.unresolved ?? 0} unresolved`,
|
|
256
294
|
`Solutions: ${summary.solutions?.total ?? 0}`,
|
|
@@ -268,7 +306,7 @@ export function registerTools(server: McpServer, ipc: IpcClient): void {
|
|
|
268
306
|
'Get pending notifications (new solutions, recurring errors, research insights).',
|
|
269
307
|
{},
|
|
270
308
|
async () => {
|
|
271
|
-
const notifications: AnyResult = await
|
|
309
|
+
const notifications: AnyResult = await call('notification.list', {});
|
|
272
310
|
if (!notifications?.length) return textResult('No pending notifications.');
|
|
273
311
|
const lines = notifications.map((n: AnyResult) =>
|
|
274
312
|
`[${n.type}] ${n.title}: ${n.message?.slice(0, 120)}`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ErrorRecord } from '../types/error.types.js';
|
|
1
2
|
import type { ErrorRepository } from '../db/repositories/error.repository.js';
|
|
2
3
|
import type { SolutionRepository } from '../db/repositories/solution.repository.js';
|
|
3
4
|
import type { CodeModuleRepository } from '../db/repositories/code-module.repository.js';
|
|
@@ -14,6 +15,7 @@ export interface ProjectSummary {
|
|
|
14
15
|
antipatterns: { total: number };
|
|
15
16
|
modules: { total: number };
|
|
16
17
|
insights: { active: number };
|
|
18
|
+
healthScore?: number;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface NetworkOverview {
|
|
@@ -69,6 +71,140 @@ export class AnalyticsService {
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
computeHealthScore(projectId?: number): number {
|
|
75
|
+
const summary = this.getSummary(projectId);
|
|
76
|
+
const networkStats = this.synapseManager.getNetworkStats();
|
|
77
|
+
|
|
78
|
+
let score = 0;
|
|
79
|
+
let maxScore = 0;
|
|
80
|
+
|
|
81
|
+
// Data Volume (30 points)
|
|
82
|
+
maxScore += 30;
|
|
83
|
+
const dataVolume = summary.errors.total + (summary.modules.total * 2) + summary.solutions.total;
|
|
84
|
+
score += Math.min(30, dataVolume * 0.3);
|
|
85
|
+
|
|
86
|
+
// Synapse Density (20 points) - more connections = richer network
|
|
87
|
+
maxScore += 20;
|
|
88
|
+
const synapseDensity = networkStats.totalSynapses / Math.max(1, networkStats.totalNodes);
|
|
89
|
+
score += Math.min(20, synapseDensity * 5);
|
|
90
|
+
|
|
91
|
+
// Solution Coverage (20 points) - resolved errors vs total
|
|
92
|
+
maxScore += 20;
|
|
93
|
+
if (summary.errors.total > 0) {
|
|
94
|
+
const resolvedRate = 1 - (summary.errors.unresolved / summary.errors.total);
|
|
95
|
+
score += resolvedRate * 20;
|
|
96
|
+
} else {
|
|
97
|
+
score += 10; // No errors = neutral
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Learning Activity (15 points) - active rules + insights
|
|
101
|
+
maxScore += 15;
|
|
102
|
+
const learningActivity = summary.rules.active + summary.insights.active;
|
|
103
|
+
score += Math.min(15, learningActivity * 1.5);
|
|
104
|
+
|
|
105
|
+
// Error Trend (15 points) - fewer recent errors = better
|
|
106
|
+
maxScore += 15;
|
|
107
|
+
if (summary.errors.total > 0) {
|
|
108
|
+
const recentRatio = summary.errors.last7d / Math.max(1, summary.errors.total);
|
|
109
|
+
// Low recent ratio = health is good (errors decreasing)
|
|
110
|
+
score += Math.max(0, 15 - recentRatio * 30);
|
|
111
|
+
} else {
|
|
112
|
+
score += 15;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Math.round((score / maxScore) * 100);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getTimeSeries(projectId?: number, days: number = 30): Array<{ date: string; errors: number; solutions: number }> {
|
|
119
|
+
const series: Array<{ date: string; errors: number; solutions: number }> = [];
|
|
120
|
+
|
|
121
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
122
|
+
const dayStart = new Date(Date.now() - (i + 1) * 86400000).toISOString();
|
|
123
|
+
const dayEnd = new Date(Date.now() - i * 86400000).toISOString();
|
|
124
|
+
|
|
125
|
+
const errorsInDay = this.errorRepo.countSince(dayStart, projectId) - this.errorRepo.countSince(dayEnd, projectId);
|
|
126
|
+
|
|
127
|
+
series.push({
|
|
128
|
+
date: dayStart.split('T')[0]!,
|
|
129
|
+
errors: Math.max(0, errorsInDay),
|
|
130
|
+
solutions: 0, // Approximation
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return series;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
explainError(errorId: number): {
|
|
138
|
+
error: ErrorRecord | undefined;
|
|
139
|
+
solutions: Array<{ id: number; description: string; confidence: number; successRate: number }>;
|
|
140
|
+
chain: { parents: ErrorRecord[]; children: ErrorRecord[] };
|
|
141
|
+
relatedErrors: Array<{ id: number; type: string; message: string; similarity: number }>;
|
|
142
|
+
rules: Array<{ id: number; pattern: string; action: string; confidence: number }>;
|
|
143
|
+
insights: Array<{ id: number; type: string; title: string }>;
|
|
144
|
+
synapseConnections: number;
|
|
145
|
+
} {
|
|
146
|
+
const error = this.errorRepo.getById(errorId);
|
|
147
|
+
if (!error) {
|
|
148
|
+
return {
|
|
149
|
+
error: undefined, solutions: [], chain: { parents: [], children: [] },
|
|
150
|
+
relatedErrors: [], rules: [], insights: [], synapseConnections: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Solutions
|
|
155
|
+
const solutions = this.solutionRepo.findForError(errorId).map(s => ({
|
|
156
|
+
id: s.id,
|
|
157
|
+
description: s.description,
|
|
158
|
+
confidence: s.confidence,
|
|
159
|
+
successRate: this.solutionRepo.successRate(s.id),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Error chain
|
|
163
|
+
const parents = this.errorRepo.findChainParents(errorId);
|
|
164
|
+
const children = this.errorRepo.findChainChildren(errorId);
|
|
165
|
+
|
|
166
|
+
// Related via synapses
|
|
167
|
+
const context = this.synapseManager.getErrorContext(errorId);
|
|
168
|
+
const relatedErrors = context.relatedErrors.map(r => {
|
|
169
|
+
const e = this.errorRepo.getById(r.node.id);
|
|
170
|
+
return {
|
|
171
|
+
id: r.node.id,
|
|
172
|
+
type: e?.type ?? 'unknown',
|
|
173
|
+
message: e?.message ?? '',
|
|
174
|
+
similarity: r.activation,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Prevention rules
|
|
179
|
+
const matchedRules = this.ruleRepo.findActive(error.project_id);
|
|
180
|
+
const rules = matchedRules
|
|
181
|
+
.filter(r => {
|
|
182
|
+
try { return new RegExp(r.pattern, 'i').test(`${error.type}: ${error.message}`); }
|
|
183
|
+
catch { return false; }
|
|
184
|
+
})
|
|
185
|
+
.map(r => ({ id: r.id, pattern: r.pattern, action: r.action, confidence: r.confidence }));
|
|
186
|
+
|
|
187
|
+
// Related insights
|
|
188
|
+
const insights = context.insights.map(i => ({
|
|
189
|
+
id: i.node.id,
|
|
190
|
+
type: i.node.type,
|
|
191
|
+
title: `Insight #${i.node.id}`,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
// Total synapse connections
|
|
195
|
+
const allConnections = this.synapseManager.activate({ type: 'error', id: errorId });
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
error,
|
|
199
|
+
solutions,
|
|
200
|
+
chain: { parents, children },
|
|
201
|
+
relatedErrors,
|
|
202
|
+
rules,
|
|
203
|
+
insights,
|
|
204
|
+
synapseConnections: allConnections.length,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
72
208
|
getNetworkOverview(limit: number = 10): NetworkOverview {
|
|
73
209
|
const stats = this.synapseManager.getNetworkStats();
|
|
74
210
|
const strongest = this.synapseManager.getStrongestSynapses(limit);
|
|
@@ -6,7 +6,7 @@ import { analyzeCode } from '../code/analyzer.js';
|
|
|
6
6
|
import { fingerprintCode } from '../code/fingerprint.js';
|
|
7
7
|
import { computeReusabilityScore } from '../code/scorer.js';
|
|
8
8
|
import { detectGranularity } from '../code/registry.js';
|
|
9
|
-
import { findExactMatches, findSemanticMatches } from '../code/matcher.js';
|
|
9
|
+
import { findExactMatches, findSemanticMatches, findStructuralMatches } from '../code/matcher.js';
|
|
10
10
|
import { sha256 } from '../utils/hash.js';
|
|
11
11
|
import { getEventBus } from '../utils/events.js';
|
|
12
12
|
import { getLogger } from '../utils/logger.js';
|
|
@@ -53,27 +53,59 @@ export class CodeService {
|
|
|
53
53
|
// Check if module already exists (by fingerprint)
|
|
54
54
|
const existing = this.codeModuleRepo.findByFingerprint(fingerprint);
|
|
55
55
|
if (existing) {
|
|
56
|
+
// Source Hash Change Detection: compare hash to decide if re-analysis needed
|
|
57
|
+
if (existing.source_hash === sourceHash) {
|
|
58
|
+
// Unchanged — skip re-analysis
|
|
59
|
+
this.logger.debug(`Module ${existing.name} unchanged (hash match), skipping`);
|
|
60
|
+
this.synapseManager.strengthen(
|
|
61
|
+
{ type: 'code_module', id: existing.id },
|
|
62
|
+
{ type: 'project', id: project.id },
|
|
63
|
+
'uses_module',
|
|
64
|
+
);
|
|
65
|
+
return { moduleId: existing.id, isNew: false, reusabilityScore: existing.reusability_score };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Hash changed — re-analyze
|
|
69
|
+
this.logger.info(`Module ${existing.name} changed (hash drift), re-analyzing`);
|
|
70
|
+
const reScore = computeReusabilityScore({
|
|
71
|
+
source: input.source,
|
|
72
|
+
filePath: input.filePath,
|
|
73
|
+
exports: analysis.exports,
|
|
74
|
+
internalDeps: analysis.internalDeps,
|
|
75
|
+
hasTypeAnnotations: analysis.hasTypeAnnotations,
|
|
76
|
+
complexity: analysis.complexity,
|
|
77
|
+
});
|
|
78
|
+
|
|
56
79
|
this.codeModuleRepo.update(existing.id, {
|
|
57
80
|
source_hash: sourceHash,
|
|
81
|
+
lines_of_code: analysis.linesOfCode,
|
|
82
|
+
complexity: analysis.complexity,
|
|
83
|
+
reusability_score: reScore,
|
|
58
84
|
updated_at: new Date().toISOString(),
|
|
59
85
|
});
|
|
60
86
|
|
|
87
|
+
// Re-index dependency synapses on change
|
|
88
|
+
this.indexDependencySynapses(existing.id, analysis.internalDeps, project.id);
|
|
89
|
+
|
|
61
90
|
this.synapseManager.strengthen(
|
|
62
91
|
{ type: 'code_module', id: existing.id },
|
|
63
92
|
{ type: 'project', id: project.id },
|
|
64
93
|
'uses_module',
|
|
65
94
|
);
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
this.eventBus.emit('module:updated', { moduleId: existing.id });
|
|
97
|
+
|
|
98
|
+
return { moduleId: existing.id, isNew: false, reusabilityScore: reScore };
|
|
68
99
|
}
|
|
69
100
|
|
|
70
|
-
// Compute reusability score
|
|
101
|
+
// Compute reusability score (with complexity)
|
|
71
102
|
const reusabilityScore = computeReusabilityScore({
|
|
72
103
|
source: input.source,
|
|
73
104
|
filePath: input.filePath,
|
|
74
105
|
exports: analysis.exports,
|
|
75
106
|
internalDeps: analysis.internalDeps,
|
|
76
107
|
hasTypeAnnotations: analysis.hasTypeAnnotations,
|
|
108
|
+
complexity: analysis.complexity,
|
|
77
109
|
});
|
|
78
110
|
|
|
79
111
|
const granularity = detectGranularity(input.source, input.language);
|
|
@@ -87,7 +119,7 @@ export class CodeService {
|
|
|
87
119
|
description: input.description ?? null,
|
|
88
120
|
source_hash: sourceHash,
|
|
89
121
|
lines_of_code: analysis.linesOfCode,
|
|
90
|
-
complexity:
|
|
122
|
+
complexity: analysis.complexity,
|
|
91
123
|
reusability_score: reusabilityScore,
|
|
92
124
|
});
|
|
93
125
|
|
|
@@ -98,6 +130,12 @@ export class CodeService {
|
|
|
98
130
|
'uses_module',
|
|
99
131
|
);
|
|
100
132
|
|
|
133
|
+
// Create dependency synapses for internal imports
|
|
134
|
+
this.indexDependencySynapses(moduleId, analysis.internalDeps, project.id);
|
|
135
|
+
|
|
136
|
+
// Compute and store pairwise module similarities
|
|
137
|
+
this.computeModuleSimilarities(moduleId, input.source, input.language);
|
|
138
|
+
|
|
101
139
|
this.eventBus.emit('module:registered', { moduleId, projectId: project.id });
|
|
102
140
|
this.logger.info(`Code module registered (id=${moduleId}, name=${input.name}, granularity=${granularity}, score=${reusabilityScore.toFixed(2)})`);
|
|
103
141
|
|
|
@@ -141,6 +179,51 @@ export class CodeService {
|
|
|
141
179
|
return this.codeModuleRepo.getById(id);
|
|
142
180
|
}
|
|
143
181
|
|
|
182
|
+
private computeModuleSimilarities(moduleId: number, source: string, language: string): void {
|
|
183
|
+
const allModules = this.codeModuleRepo.findByLanguage(language, 100);
|
|
184
|
+
const candidates = allModules.filter(m => m.id !== moduleId);
|
|
185
|
+
if (candidates.length === 0) return;
|
|
186
|
+
|
|
187
|
+
const matches = findStructuralMatches(source, language, candidates, 0.3);
|
|
188
|
+
|
|
189
|
+
for (const match of matches) {
|
|
190
|
+
if (match.score >= 0.3 && match.moduleId !== moduleId) {
|
|
191
|
+
this.codeModuleRepo.upsertSimilarity(moduleId, match.moduleId, match.score);
|
|
192
|
+
|
|
193
|
+
// High similarity → create synapse
|
|
194
|
+
if (match.score >= 0.7) {
|
|
195
|
+
this.synapseManager.strengthen(
|
|
196
|
+
{ type: 'code_module', id: moduleId },
|
|
197
|
+
{ type: 'code_module', id: match.moduleId },
|
|
198
|
+
'similar_to',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private indexDependencySynapses(moduleId: number, internalDeps: string[], projectId: number): void {
|
|
206
|
+
const projectModules = this.codeModuleRepo.findByProject(projectId);
|
|
207
|
+
|
|
208
|
+
for (const dep of internalDeps) {
|
|
209
|
+
// Normalize the dep path to match against registered modules
|
|
210
|
+
const depName = dep.replace(/^\.\//, '').replace(/\.\w+$/, '');
|
|
211
|
+
|
|
212
|
+
const target = projectModules.find(m => {
|
|
213
|
+
const modulePath = m.file_path.replace(/\\/g, '/').replace(/\.\w+$/, '');
|
|
214
|
+
return modulePath.endsWith(depName) || m.name === depName;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (target && target.id !== moduleId) {
|
|
218
|
+
this.synapseManager.strengthen(
|
|
219
|
+
{ type: 'code_module', id: moduleId },
|
|
220
|
+
{ type: 'code_module', id: target.id },
|
|
221
|
+
'depends_on',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
144
227
|
listProjects(): Array<{ id: number; name: string; path: string | null; language: string | null; framework: string | null; moduleCount: number }> {
|
|
145
228
|
const projects = this.projectRepo.getAll();
|
|
146
229
|
return projects.map(p => ({
|