@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.
Files changed (131) 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/explain.d.ts +2 -0
  11. package/dist/cli/commands/explain.js +76 -0
  12. package/dist/cli/commands/explain.js.map +1 -0
  13. package/dist/code/analyzer.d.ts +6 -0
  14. package/dist/code/analyzer.js +35 -0
  15. package/dist/code/analyzer.js.map +1 -1
  16. package/dist/code/matcher.d.ts +11 -1
  17. package/dist/code/matcher.js +49 -0
  18. package/dist/code/matcher.js.map +1 -1
  19. package/dist/code/scorer.d.ts +1 -0
  20. package/dist/code/scorer.js +15 -1
  21. package/dist/code/scorer.js.map +1 -1
  22. package/dist/config.js +31 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/dashboard/server.d.ts +15 -0
  25. package/dist/dashboard/server.js +124 -0
  26. package/dist/dashboard/server.js.map +1 -0
  27. package/dist/db/migrations/007_feedback.d.ts +2 -0
  28. package/dist/db/migrations/007_feedback.js +12 -0
  29. package/dist/db/migrations/007_feedback.js.map +1 -0
  30. package/dist/db/migrations/008_git_integration.d.ts +2 -0
  31. package/dist/db/migrations/008_git_integration.js +37 -0
  32. package/dist/db/migrations/008_git_integration.js.map +1 -0
  33. package/dist/db/migrations/009_embeddings.d.ts +2 -0
  34. package/dist/db/migrations/009_embeddings.js +7 -0
  35. package/dist/db/migrations/009_embeddings.js.map +1 -0
  36. package/dist/db/migrations/index.js +6 -0
  37. package/dist/db/migrations/index.js.map +1 -1
  38. package/dist/db/repositories/code-module.repository.d.ts +16 -0
  39. package/dist/db/repositories/code-module.repository.js +42 -0
  40. package/dist/db/repositories/code-module.repository.js.map +1 -1
  41. package/dist/db/repositories/error.repository.d.ts +5 -0
  42. package/dist/db/repositories/error.repository.js +27 -0
  43. package/dist/db/repositories/error.repository.js.map +1 -1
  44. package/dist/db/repositories/insight.repository.d.ts +2 -0
  45. package/dist/db/repositories/insight.repository.js +13 -0
  46. package/dist/db/repositories/insight.repository.js.map +1 -1
  47. package/dist/embeddings/engine.d.ts +42 -0
  48. package/dist/embeddings/engine.js +166 -0
  49. package/dist/embeddings/engine.js.map +1 -0
  50. package/dist/hooks/post-tool-use.js +2 -0
  51. package/dist/hooks/post-tool-use.js.map +1 -1
  52. package/dist/hooks/post-write.js +11 -0
  53. package/dist/hooks/post-write.js.map +1 -1
  54. package/dist/index.js +3 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/ipc/router.d.ts +2 -0
  57. package/dist/ipc/router.js +13 -0
  58. package/dist/ipc/router.js.map +1 -1
  59. package/dist/learning/confidence-scorer.d.ts +16 -0
  60. package/dist/learning/confidence-scorer.js +20 -0
  61. package/dist/learning/confidence-scorer.js.map +1 -1
  62. package/dist/learning/learning-engine.js +12 -5
  63. package/dist/learning/learning-engine.js.map +1 -1
  64. package/dist/matching/error-matcher.d.ts +9 -1
  65. package/dist/matching/error-matcher.js +50 -5
  66. package/dist/matching/error-matcher.js.map +1 -1
  67. package/dist/mcp/http-server.d.ts +14 -0
  68. package/dist/mcp/http-server.js +117 -0
  69. package/dist/mcp/http-server.js.map +1 -0
  70. package/dist/mcp/tools.d.ts +4 -0
  71. package/dist/mcp/tools.js +41 -14
  72. package/dist/mcp/tools.js.map +1 -1
  73. package/dist/services/analytics.service.d.ts +39 -0
  74. package/dist/services/analytics.service.js +111 -0
  75. package/dist/services/analytics.service.js.map +1 -1
  76. package/dist/services/code.service.d.ts +2 -0
  77. package/dist/services/code.service.js +62 -4
  78. package/dist/services/code.service.js.map +1 -1
  79. package/dist/services/error.service.d.ts +17 -1
  80. package/dist/services/error.service.js +90 -12
  81. package/dist/services/error.service.js.map +1 -1
  82. package/dist/services/git.service.d.ts +49 -0
  83. package/dist/services/git.service.js +112 -0
  84. package/dist/services/git.service.js.map +1 -0
  85. package/dist/services/prevention.service.d.ts +7 -0
  86. package/dist/services/prevention.service.js +38 -0
  87. package/dist/services/prevention.service.js.map +1 -1
  88. package/dist/services/research.service.d.ts +1 -0
  89. package/dist/services/research.service.js +4 -0
  90. package/dist/services/research.service.js.map +1 -1
  91. package/dist/services/solution.service.d.ts +10 -0
  92. package/dist/services/solution.service.js +48 -0
  93. package/dist/services/solution.service.js.map +1 -1
  94. package/dist/types/config.types.d.ts +21 -0
  95. package/dist/types/synapse.types.d.ts +1 -1
  96. package/package.json +8 -3
  97. package/src/api/server.ts +321 -0
  98. package/src/brain.ts +50 -8
  99. package/src/cli/commands/dashboard.ts +2 -0
  100. package/src/cli/commands/explain.ts +83 -0
  101. package/src/code/analyzer.ts +40 -0
  102. package/src/code/matcher.ts +67 -2
  103. package/src/code/scorer.ts +13 -1
  104. package/src/config.ts +24 -0
  105. package/src/dashboard/server.ts +142 -0
  106. package/src/db/migrations/007_feedback.ts +13 -0
  107. package/src/db/migrations/008_git_integration.ts +38 -0
  108. package/src/db/migrations/009_embeddings.ts +8 -0
  109. package/src/db/migrations/index.ts +6 -0
  110. package/src/db/repositories/code-module.repository.ts +53 -0
  111. package/src/db/repositories/error.repository.ts +40 -0
  112. package/src/db/repositories/insight.repository.ts +21 -0
  113. package/src/embeddings/engine.ts +217 -0
  114. package/src/hooks/post-tool-use.ts +2 -0
  115. package/src/hooks/post-write.ts +12 -0
  116. package/src/index.ts +3 -1
  117. package/src/ipc/router.ts +16 -0
  118. package/src/learning/confidence-scorer.ts +33 -0
  119. package/src/learning/learning-engine.ts +13 -5
  120. package/src/matching/error-matcher.ts +55 -4
  121. package/src/mcp/http-server.ts +137 -0
  122. package/src/mcp/tools.ts +52 -14
  123. package/src/services/analytics.service.ts +136 -0
  124. package/src/services/code.service.ts +87 -4
  125. package/src/services/error.service.ts +114 -13
  126. package/src/services/git.service.ts +132 -0
  127. package/src/services/prevention.service.ts +40 -0
  128. package/src/services/research.service.ts +5 -0
  129. package/src/services/solution.service.ts +58 -0
  130. package/src/types/config.types.ts +24 -0
  131. 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 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);
@@ -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
- return { moduleId: existing.id, isNew: false, reusabilityScore: existing.reusability_score };
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: null,
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 => ({