@timmeck/brain 1.1.1 → 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.
Files changed (139) hide show
  1. package/README.md +225 -50
  2. package/dist/api/server.d.ts +19 -0
  3. package/dist/api/server.js +281 -0
  4. package/dist/api/server.js.map +1 -0
  5. package/dist/brain.d.ts +3 -0
  6. package/dist/brain.js +45 -8
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +2 -0
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +2 -0
  11. package/dist/cli/commands/doctor.js +118 -0
  12. package/dist/cli/commands/doctor.js.map +1 -0
  13. package/dist/cli/commands/explain.d.ts +2 -0
  14. package/dist/cli/commands/explain.js +76 -0
  15. package/dist/cli/commands/explain.js.map +1 -0
  16. package/dist/cli/commands/projects.d.ts +2 -0
  17. package/dist/cli/commands/projects.js +36 -0
  18. package/dist/cli/commands/projects.js.map +1 -0
  19. package/dist/code/analyzer.d.ts +6 -0
  20. package/dist/code/analyzer.js +35 -0
  21. package/dist/code/analyzer.js.map +1 -1
  22. package/dist/code/matcher.d.ts +11 -1
  23. package/dist/code/matcher.js +49 -0
  24. package/dist/code/matcher.js.map +1 -1
  25. package/dist/code/scorer.d.ts +1 -0
  26. package/dist/code/scorer.js +15 -1
  27. package/dist/code/scorer.js.map +1 -1
  28. package/dist/config.js +31 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/dashboard/server.d.ts +15 -0
  31. package/dist/dashboard/server.js +124 -0
  32. package/dist/dashboard/server.js.map +1 -0
  33. package/dist/db/migrations/007_feedback.d.ts +2 -0
  34. package/dist/db/migrations/007_feedback.js +12 -0
  35. package/dist/db/migrations/007_feedback.js.map +1 -0
  36. package/dist/db/migrations/008_git_integration.d.ts +2 -0
  37. package/dist/db/migrations/008_git_integration.js +37 -0
  38. package/dist/db/migrations/008_git_integration.js.map +1 -0
  39. package/dist/db/migrations/009_embeddings.d.ts +2 -0
  40. package/dist/db/migrations/009_embeddings.js +7 -0
  41. package/dist/db/migrations/009_embeddings.js.map +1 -0
  42. package/dist/db/migrations/index.js +6 -0
  43. package/dist/db/migrations/index.js.map +1 -1
  44. package/dist/db/repositories/code-module.repository.d.ts +16 -0
  45. package/dist/db/repositories/code-module.repository.js +42 -0
  46. package/dist/db/repositories/code-module.repository.js.map +1 -1
  47. package/dist/db/repositories/error.repository.d.ts +5 -0
  48. package/dist/db/repositories/error.repository.js +27 -0
  49. package/dist/db/repositories/error.repository.js.map +1 -1
  50. package/dist/db/repositories/insight.repository.d.ts +2 -0
  51. package/dist/db/repositories/insight.repository.js +13 -0
  52. package/dist/db/repositories/insight.repository.js.map +1 -1
  53. package/dist/embeddings/engine.d.ts +42 -0
  54. package/dist/embeddings/engine.js +166 -0
  55. package/dist/embeddings/engine.js.map +1 -0
  56. package/dist/hooks/post-tool-use.js +2 -0
  57. package/dist/hooks/post-tool-use.js.map +1 -1
  58. package/dist/hooks/post-write.js +11 -0
  59. package/dist/hooks/post-write.js.map +1 -1
  60. package/dist/index.js +7 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/ipc/router.d.ts +2 -0
  63. package/dist/ipc/router.js +15 -0
  64. package/dist/ipc/router.js.map +1 -1
  65. package/dist/learning/confidence-scorer.d.ts +16 -0
  66. package/dist/learning/confidence-scorer.js +20 -0
  67. package/dist/learning/confidence-scorer.js.map +1 -1
  68. package/dist/learning/learning-engine.js +12 -5
  69. package/dist/learning/learning-engine.js.map +1 -1
  70. package/dist/matching/error-matcher.d.ts +9 -1
  71. package/dist/matching/error-matcher.js +50 -5
  72. package/dist/matching/error-matcher.js.map +1 -1
  73. package/dist/mcp/http-server.d.ts +14 -0
  74. package/dist/mcp/http-server.js +117 -0
  75. package/dist/mcp/http-server.js.map +1 -0
  76. package/dist/mcp/tools.d.ts +4 -0
  77. package/dist/mcp/tools.js +41 -14
  78. package/dist/mcp/tools.js.map +1 -1
  79. package/dist/services/analytics.service.d.ts +39 -0
  80. package/dist/services/analytics.service.js +111 -0
  81. package/dist/services/analytics.service.js.map +1 -1
  82. package/dist/services/code.service.d.ts +10 -0
  83. package/dist/services/code.service.js +73 -4
  84. package/dist/services/code.service.js.map +1 -1
  85. package/dist/services/error.service.d.ts +17 -1
  86. package/dist/services/error.service.js +90 -12
  87. package/dist/services/error.service.js.map +1 -1
  88. package/dist/services/git.service.d.ts +49 -0
  89. package/dist/services/git.service.js +112 -0
  90. package/dist/services/git.service.js.map +1 -0
  91. package/dist/services/prevention.service.d.ts +7 -0
  92. package/dist/services/prevention.service.js +38 -0
  93. package/dist/services/prevention.service.js.map +1 -1
  94. package/dist/services/research.service.d.ts +1 -0
  95. package/dist/services/research.service.js +4 -0
  96. package/dist/services/research.service.js.map +1 -1
  97. package/dist/services/solution.service.d.ts +10 -0
  98. package/dist/services/solution.service.js +48 -0
  99. package/dist/services/solution.service.js.map +1 -1
  100. package/dist/types/config.types.d.ts +21 -0
  101. package/dist/types/synapse.types.d.ts +1 -1
  102. package/package.json +8 -3
  103. package/src/api/server.ts +321 -0
  104. package/src/brain.ts +50 -8
  105. package/src/cli/commands/dashboard.ts +2 -0
  106. package/src/cli/commands/doctor.ts +118 -0
  107. package/src/cli/commands/explain.ts +83 -0
  108. package/src/cli/commands/projects.ts +42 -0
  109. package/src/code/analyzer.ts +40 -0
  110. package/src/code/matcher.ts +67 -2
  111. package/src/code/scorer.ts +13 -1
  112. package/src/config.ts +24 -0
  113. package/src/dashboard/server.ts +142 -0
  114. package/src/db/migrations/007_feedback.ts +13 -0
  115. package/src/db/migrations/008_git_integration.ts +38 -0
  116. package/src/db/migrations/009_embeddings.ts +8 -0
  117. package/src/db/migrations/index.ts +6 -0
  118. package/src/db/repositories/code-module.repository.ts +53 -0
  119. package/src/db/repositories/error.repository.ts +40 -0
  120. package/src/db/repositories/insight.repository.ts +21 -0
  121. package/src/embeddings/engine.ts +217 -0
  122. package/src/hooks/post-tool-use.ts +2 -0
  123. package/src/hooks/post-write.ts +12 -0
  124. package/src/index.ts +7 -1
  125. package/src/ipc/router.ts +19 -0
  126. package/src/learning/confidence-scorer.ts +33 -0
  127. package/src/learning/learning-engine.ts +13 -5
  128. package/src/matching/error-matcher.ts +55 -4
  129. package/src/mcp/http-server.ts +137 -0
  130. package/src/mcp/tools.ts +52 -14
  131. package/src/services/analytics.service.ts +136 -0
  132. package/src/services/code.service.ts +99 -4
  133. package/src/services/error.service.ts +114 -13
  134. package/src/services/git.service.ts +132 -0
  135. package/src/services/prevention.service.ts +40 -0
  136. package/src/services/research.service.ts +5 -0
  137. package/src/services/solution.service.ts +58 -0
  138. package/src/types/config.types.ts +24 -0
  139. package/src/types/synapse.types.ts +1 -0
@@ -21,7 +21,8 @@ interface MatchSignal {
21
21
  compute: (a: ErrorRecord, b: ErrorRecord) => number;
22
22
  }
23
23
 
24
- const SIGNALS: MatchSignal[] = [
24
+ // Base signals (used when vector search is NOT available)
25
+ const SIGNALS_BASE: MatchSignal[] = [
25
26
  { name: 'fingerprint', weight: 0.30, compute: fingerprintMatch },
26
27
  { name: 'message_similarity', weight: 0.20, compute: messageSimilarity },
27
28
  { name: 'type_match', weight: 0.15, compute: typeMatch },
@@ -30,16 +31,41 @@ const SIGNALS: MatchSignal[] = [
30
31
  { name: 'context_similarity', weight: 0.10, compute: contextSimilarity },
31
32
  ];
32
33
 
34
+ // Hybrid signals (used when vector search IS available — vector gets 20% weight)
35
+ const SIGNALS_HYBRID: MatchSignal[] = [
36
+ { name: 'fingerprint', weight: 0.25, compute: fingerprintMatch },
37
+ { name: 'message_similarity', weight: 0.15, compute: messageSimilarity },
38
+ { name: 'type_match', weight: 0.12, compute: typeMatch },
39
+ { name: 'stack_similarity', weight: 0.12, compute: stackSimilarity },
40
+ { name: 'file_similarity', weight: 0.08, compute: fileSimilarity },
41
+ { name: 'context_similarity', weight: 0.08, compute: contextSimilarity },
42
+ ];
43
+
44
+ const VECTOR_WEIGHT = 0.20;
33
45
  const MATCH_THRESHOLD = 0.70;
34
46
  const STRONG_MATCH_THRESHOLD = 0.90;
35
47
 
48
+ /**
49
+ * Hybrid error matching: TF-IDF signals + optional vector similarity + synapse boost.
50
+ *
51
+ * @param incoming - The error to match
52
+ * @param candidates - Candidate errors to compare against
53
+ * @param vectorScores - Pre-computed vector similarity scores (errorId → score)
54
+ * @param synapseScores - Pre-computed synapse proximity scores (errorId → score)
55
+ */
36
56
  export function matchError(
37
57
  incoming: ErrorRecord,
38
58
  candidates: ErrorRecord[],
59
+ vectorScores?: Map<number, number>,
60
+ synapseScores?: Map<number, number>,
39
61
  ): MatchResult[] {
62
+ const useHybrid = vectorScores && vectorScores.size > 0;
63
+ const useSynapse = synapseScores && synapseScores.size > 0;
64
+ const signals = useHybrid ? SIGNALS_HYBRID : SIGNALS_BASE;
65
+
40
66
  return candidates
41
67
  .map(candidate => {
42
- const signals = SIGNALS.map(signal => {
68
+ const signalResults = signals.map(signal => {
43
69
  const score = signal.compute(incoming, candidate);
44
70
  return {
45
71
  signal: signal.name,
@@ -48,12 +74,37 @@ export function matchError(
48
74
  };
49
75
  });
50
76
 
51
- const totalScore = signals.reduce((sum, s) => sum + s.weighted, 0);
77
+ // Add vector similarity signal (if available)
78
+ if (useHybrid) {
79
+ const vectorScore = vectorScores.get(candidate.id) ?? 0;
80
+ signalResults.push({
81
+ signal: 'vector_similarity',
82
+ score: vectorScore,
83
+ weighted: vectorScore * VECTOR_WEIGHT,
84
+ });
85
+ }
86
+
87
+ let totalScore = signalResults.reduce((sum, s) => sum + s.weighted, 0);
88
+
89
+ // Synapse boost: if errors are already connected in the synapse network,
90
+ // give up to 5% bonus (doesn't create false positives, only reinforces)
91
+ if (useSynapse) {
92
+ const synapseScore = synapseScores.get(candidate.id) ?? 0;
93
+ if (synapseScore > 0) {
94
+ const bonus = Math.min(synapseScore * 0.05, 0.05);
95
+ totalScore = Math.min(1.0, totalScore + bonus);
96
+ signalResults.push({
97
+ signal: 'synapse_boost',
98
+ score: synapseScore,
99
+ weighted: bonus,
100
+ });
101
+ }
102
+ }
52
103
 
53
104
  return {
54
105
  errorId: candidate.id,
55
106
  score: totalScore,
56
- signals,
107
+ signals: signalResults,
57
108
  isStrong: totalScore >= STRONG_MATCH_THRESHOLD,
58
109
  };
59
110
  })
@@ -0,0 +1,137 @@
1
+ import http from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
+ import { getLogger } from '../utils/logger.js';
6
+ import type { IpcRouter } from '../ipc/router.js';
7
+ import { registerToolsDirect } from './tools.js';
8
+
9
+ export class McpHttpServer {
10
+ private server: http.Server | null = null;
11
+ private transports = new Map<string, SSEServerTransport>();
12
+ private logger = getLogger();
13
+
14
+ constructor(
15
+ private port: number,
16
+ private router: IpcRouter,
17
+ ) {}
18
+
19
+ start(): void {
20
+ this.server = http.createServer((req, res) => {
21
+ res.setHeader('Access-Control-Allow-Origin', '*');
22
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
23
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
24
+
25
+ if (req.method === 'OPTIONS') {
26
+ res.writeHead(204);
27
+ res.end();
28
+ return;
29
+ }
30
+
31
+ const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
32
+
33
+ if (url.pathname === '/sse' && req.method === 'GET') {
34
+ this.handleSSE(res);
35
+ return;
36
+ }
37
+
38
+ if (url.pathname === '/messages' && req.method === 'POST') {
39
+ this.handleMessage(req, res, url);
40
+ return;
41
+ }
42
+
43
+ if (url.pathname === '/' && req.method === 'GET') {
44
+ res.writeHead(200, { 'Content-Type': 'application/json' });
45
+ res.end(JSON.stringify({
46
+ name: 'brain',
47
+ version: '1.7.0',
48
+ protocol: 'MCP',
49
+ transport: 'sse',
50
+ endpoints: {
51
+ sse: '/sse',
52
+ messages: '/messages',
53
+ },
54
+ clients: this.transports.size,
55
+ }));
56
+ return;
57
+ }
58
+
59
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
60
+ res.end('Not Found');
61
+ });
62
+
63
+ this.server.listen(this.port, () => {
64
+ this.logger.info(`MCP HTTP server (SSE) started on http://localhost:${this.port}`);
65
+ });
66
+ }
67
+
68
+ stop(): void {
69
+ this.transports.clear();
70
+ this.server?.close();
71
+ this.server = null;
72
+ this.logger.info('MCP HTTP server stopped');
73
+ }
74
+
75
+ getClientCount(): number {
76
+ return this.transports.size;
77
+ }
78
+
79
+ private async handleSSE(res: http.ServerResponse): Promise<void> {
80
+ try {
81
+ const transport = new SSEServerTransport('/messages', res);
82
+ const sessionId = transport.sessionId ?? randomUUID();
83
+ this.transports.set(sessionId, transport);
84
+
85
+ const server = new McpServer({
86
+ name: 'brain',
87
+ version: '1.7.0',
88
+ });
89
+
90
+ registerToolsDirect(server, this.router);
91
+
92
+ res.on('close', () => {
93
+ this.transports.delete(sessionId);
94
+ this.logger.debug(`MCP SSE client disconnected: ${sessionId}`);
95
+ });
96
+
97
+ await server.connect(transport);
98
+ this.logger.info(`MCP SSE client connected: ${sessionId}`);
99
+ } catch (err) {
100
+ this.logger.error('MCP SSE connection error:', err);
101
+ if (!res.headersSent) {
102
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
103
+ res.end('Internal Server Error');
104
+ }
105
+ }
106
+ }
107
+
108
+ private async handleMessage(
109
+ req: http.IncomingMessage,
110
+ res: http.ServerResponse,
111
+ url: URL,
112
+ ): Promise<void> {
113
+ try {
114
+ const sessionId = url.searchParams.get('sessionId');
115
+ if (!sessionId) {
116
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
117
+ res.end('Missing sessionId parameter');
118
+ return;
119
+ }
120
+
121
+ const transport = this.transports.get(sessionId);
122
+ if (!transport) {
123
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
124
+ res.end('Session not found. Connect to /sse first.');
125
+ return;
126
+ }
127
+
128
+ await transport.handlePostMessage(req, res);
129
+ } catch (err) {
130
+ this.logger.error('MCP message error:', err);
131
+ if (!res.headersSent) {
132
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
133
+ res.end('Internal Server Error');
134
+ }
135
+ }
136
+ }
137
+ }
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 ipc.request('error.report', {
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 ipc.request('error.query', {
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 ipc.request('solution.report', {
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 ipc.request('solution.rate', {
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 ipc.request('code.find', {
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 ipc.request('code.analyze', {
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 ipc.request('code.similarity', {
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 ipc.request('synapse.context', {
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 ipc.request('synapse.path', params);
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 ipc.request('research.insights', {
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 ipc.request('research.suggest', {
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 ipc.request('analytics.summary', {});
253
- const network: AnyResult = await ipc.request('synapse.stats', {});
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 ipc.request('notification.list', {});
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);