@claude-flow/plugin-code-intelligence 3.0.0-alpha.1
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 +381 -0
- package/dist/bridges/gnn-bridge.d.ts +96 -0
- package/dist/bridges/gnn-bridge.d.ts.map +1 -0
- package/dist/bridges/gnn-bridge.js +527 -0
- package/dist/bridges/gnn-bridge.js.map +1 -0
- package/dist/bridges/index.d.ts +8 -0
- package/dist/bridges/index.d.ts.map +1 -0
- package/dist/bridges/index.js +8 -0
- package/dist/bridges/index.js.map +1 -0
- package/dist/bridges/mincut-bridge.d.ts +81 -0
- package/dist/bridges/mincut-bridge.d.ts.map +1 -0
- package/dist/bridges/mincut-bridge.js +481 -0
- package/dist/bridges/mincut-bridge.js.map +1 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-tools.d.ts +98 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +794 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/types.d.ts +838 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +268 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Intelligence Plugin - MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Implements 5 MCP tools for advanced code analysis:
|
|
5
|
+
* 1. code/semantic-search - Find semantically similar code patterns
|
|
6
|
+
* 2. code/architecture-analyze - Analyze codebase architecture
|
|
7
|
+
* 3. code/refactor-impact - Predict refactoring impact using GNN
|
|
8
|
+
* 4. code/split-suggest - Suggest module splits using MinCut
|
|
9
|
+
* 5. code/learn-patterns - Learn patterns from code history
|
|
10
|
+
*
|
|
11
|
+
* Based on ADR-035: Advanced Code Intelligence Plugin
|
|
12
|
+
*
|
|
13
|
+
* @module v3/plugins/code-intelligence/mcp-tools
|
|
14
|
+
*/
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { SemanticSearchInputSchema, ArchitectureAnalyzeInputSchema, RefactorImpactInputSchema, SplitSuggestInputSchema, LearnPatternsInputSchema, CodeIntelligenceError, CodeIntelligenceErrorCodes, maskSecrets, } from './types.js';
|
|
17
|
+
import { createGNNBridge } from './bridges/gnn-bridge.js';
|
|
18
|
+
import { createMinCutBridge } from './bridges/mincut-bridge.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Security Utilities
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Validate path for security
|
|
24
|
+
*/
|
|
25
|
+
function validatePath(userPath, allowedRoots) {
|
|
26
|
+
const normalized = path.normalize(userPath);
|
|
27
|
+
// Check for path traversal
|
|
28
|
+
if (normalized.includes('..')) {
|
|
29
|
+
throw new CodeIntelligenceError(CodeIntelligenceErrorCodes.PATH_TRAVERSAL, 'Path traversal detected', { path: userPath });
|
|
30
|
+
}
|
|
31
|
+
// Check against allowed roots
|
|
32
|
+
const resolved = path.resolve(normalized);
|
|
33
|
+
const isAllowed = allowedRoots.some(root => {
|
|
34
|
+
const resolvedRoot = path.resolve(root);
|
|
35
|
+
return resolved.startsWith(resolvedRoot);
|
|
36
|
+
});
|
|
37
|
+
if (!isAllowed && allowedRoots.length > 0 && !allowedRoots.includes('.')) {
|
|
38
|
+
throw new CodeIntelligenceError(CodeIntelligenceErrorCodes.PATH_TRAVERSAL, 'Path outside allowed roots', { path: userPath, allowedRoots });
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if path is sensitive
|
|
44
|
+
*/
|
|
45
|
+
function isSensitivePath(filePath, blockedPatterns) {
|
|
46
|
+
return blockedPatterns.some(pattern => pattern.test(filePath));
|
|
47
|
+
}
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Semantic Search Tool
|
|
50
|
+
// ============================================================================
|
|
51
|
+
/**
|
|
52
|
+
* MCP Tool: code/semantic-search
|
|
53
|
+
*
|
|
54
|
+
* Search for semantically similar code patterns
|
|
55
|
+
*/
|
|
56
|
+
export const semanticSearchTool = {
|
|
57
|
+
name: 'code/semantic-search',
|
|
58
|
+
description: 'Search for semantically similar code patterns',
|
|
59
|
+
category: 'code-intelligence',
|
|
60
|
+
version: '3.0.0-alpha.1',
|
|
61
|
+
inputSchema: SemanticSearchInputSchema,
|
|
62
|
+
handler: async (input, context) => {
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
try {
|
|
65
|
+
const validated = SemanticSearchInputSchema.parse(input);
|
|
66
|
+
// Validate paths
|
|
67
|
+
const paths = validated.scope?.paths?.map(p => validatePath(p, context.config.allowedRoots)) ?? ['.'];
|
|
68
|
+
// Filter out sensitive files
|
|
69
|
+
const safePaths = paths.filter(p => !isSensitivePath(p, context.config.blockedPatterns));
|
|
70
|
+
// Initialize GNN bridge for semantic embeddings
|
|
71
|
+
const gnn = context.bridges.gnn;
|
|
72
|
+
if (!gnn.isInitialized()) {
|
|
73
|
+
await gnn.initialize();
|
|
74
|
+
}
|
|
75
|
+
// Perform search (simplified - in production would use vector index)
|
|
76
|
+
const results = await performSemanticSearch(validated.query, safePaths, validated.searchType, validated.topK, validated.scope?.languages, validated.scope?.excludeTests ?? false, context);
|
|
77
|
+
// Mask secrets in results
|
|
78
|
+
if (context.config.maskSecrets) {
|
|
79
|
+
for (const result of results) {
|
|
80
|
+
result.snippet = maskSecrets(result.snippet);
|
|
81
|
+
result.context = maskSecrets(result.context);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const result = {
|
|
85
|
+
success: true,
|
|
86
|
+
query: validated.query,
|
|
87
|
+
searchType: validated.searchType,
|
|
88
|
+
results,
|
|
89
|
+
totalMatches: results.length,
|
|
90
|
+
scope: {
|
|
91
|
+
paths: safePaths,
|
|
92
|
+
languages: validated.scope?.languages,
|
|
93
|
+
excludeTests: validated.scope?.excludeTests,
|
|
94
|
+
},
|
|
95
|
+
durationMs: Date.now() - startTime,
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
99
|
+
data: result,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
success: false,
|
|
109
|
+
error: errorMessage,
|
|
110
|
+
durationMs: Date.now() - startTime,
|
|
111
|
+
}, null, 2),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Architecture Analyze Tool
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* MCP Tool: code/architecture-analyze
|
|
122
|
+
*
|
|
123
|
+
* Analyze codebase architecture and detect drift
|
|
124
|
+
*/
|
|
125
|
+
export const architectureAnalyzeTool = {
|
|
126
|
+
name: 'code/architecture-analyze',
|
|
127
|
+
description: 'Analyze codebase architecture and detect drift',
|
|
128
|
+
category: 'code-intelligence',
|
|
129
|
+
version: '3.0.0-alpha.1',
|
|
130
|
+
inputSchema: ArchitectureAnalyzeInputSchema,
|
|
131
|
+
handler: async (input, context) => {
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
try {
|
|
134
|
+
const validated = ArchitectureAnalyzeInputSchema.parse(input);
|
|
135
|
+
// Validate root path
|
|
136
|
+
const rootPath = validatePath(validated.rootPath, context.config.allowedRoots);
|
|
137
|
+
// Initialize GNN bridge
|
|
138
|
+
const gnn = context.bridges.gnn;
|
|
139
|
+
if (!gnn.isInitialized()) {
|
|
140
|
+
await gnn.initialize();
|
|
141
|
+
}
|
|
142
|
+
// Determine analyses to perform
|
|
143
|
+
const analyses = validated.analysis ?? [
|
|
144
|
+
'dependency_graph',
|
|
145
|
+
'circular_deps',
|
|
146
|
+
'component_coupling',
|
|
147
|
+
];
|
|
148
|
+
// Build dependency graph
|
|
149
|
+
const files = await getFilesInPath(rootPath);
|
|
150
|
+
const safeFiles = files.filter(f => !isSensitivePath(f, context.config.blockedPatterns));
|
|
151
|
+
const dependencyGraph = await gnn.buildCodeGraph(safeFiles, true);
|
|
152
|
+
// Perform requested analyses
|
|
153
|
+
const result = {
|
|
154
|
+
success: true,
|
|
155
|
+
rootPath,
|
|
156
|
+
analyses: analyses,
|
|
157
|
+
dependencyGraph: analyses.includes('dependency_graph') ? dependencyGraph : undefined,
|
|
158
|
+
layerViolations: analyses.includes('layer_violations')
|
|
159
|
+
? detectLayerViolations(dependencyGraph, validated.layers)
|
|
160
|
+
: undefined,
|
|
161
|
+
circularDeps: analyses.includes('circular_deps')
|
|
162
|
+
? detectCircularDeps(dependencyGraph)
|
|
163
|
+
: undefined,
|
|
164
|
+
couplingMetrics: analyses.includes('component_coupling')
|
|
165
|
+
? calculateCouplingMetrics(dependencyGraph)
|
|
166
|
+
: undefined,
|
|
167
|
+
cohesionMetrics: analyses.includes('module_cohesion')
|
|
168
|
+
? calculateCohesionMetrics(dependencyGraph)
|
|
169
|
+
: undefined,
|
|
170
|
+
deadCode: analyses.includes('dead_code')
|
|
171
|
+
? findDeadCode(dependencyGraph)
|
|
172
|
+
: undefined,
|
|
173
|
+
apiSurface: analyses.includes('api_surface')
|
|
174
|
+
? analyzeAPISurface(dependencyGraph)
|
|
175
|
+
: undefined,
|
|
176
|
+
drift: analyses.includes('architectural_drift') && validated.baseline
|
|
177
|
+
? await detectDrift(dependencyGraph, validated.baseline)
|
|
178
|
+
: undefined,
|
|
179
|
+
summary: {
|
|
180
|
+
totalFiles: dependencyGraph.nodes.length,
|
|
181
|
+
totalModules: countModules(dependencyGraph),
|
|
182
|
+
healthScore: calculateHealthScore(dependencyGraph),
|
|
183
|
+
issues: 0,
|
|
184
|
+
warnings: 0,
|
|
185
|
+
},
|
|
186
|
+
durationMs: Date.now() - startTime,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
190
|
+
data: result,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
195
|
+
return {
|
|
196
|
+
content: [{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: JSON.stringify({
|
|
199
|
+
success: false,
|
|
200
|
+
error: errorMessage,
|
|
201
|
+
durationMs: Date.now() - startTime,
|
|
202
|
+
}, null, 2),
|
|
203
|
+
}],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Refactor Impact Tool
|
|
210
|
+
// ============================================================================
|
|
211
|
+
/**
|
|
212
|
+
* MCP Tool: code/refactor-impact
|
|
213
|
+
*
|
|
214
|
+
* Analyze impact of proposed code changes using GNN
|
|
215
|
+
*/
|
|
216
|
+
export const refactorImpactTool = {
|
|
217
|
+
name: 'code/refactor-impact',
|
|
218
|
+
description: 'Analyze impact of proposed code changes using GNN',
|
|
219
|
+
category: 'code-intelligence',
|
|
220
|
+
version: '3.0.0-alpha.1',
|
|
221
|
+
inputSchema: RefactorImpactInputSchema,
|
|
222
|
+
handler: async (input, context) => {
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
try {
|
|
225
|
+
const validated = RefactorImpactInputSchema.parse(input);
|
|
226
|
+
// Validate file paths
|
|
227
|
+
for (const change of validated.changes) {
|
|
228
|
+
validatePath(change.file, context.config.allowedRoots);
|
|
229
|
+
}
|
|
230
|
+
// Initialize GNN bridge
|
|
231
|
+
const gnn = context.bridges.gnn;
|
|
232
|
+
if (!gnn.isInitialized()) {
|
|
233
|
+
await gnn.initialize();
|
|
234
|
+
}
|
|
235
|
+
// Get affected files
|
|
236
|
+
const changedFiles = validated.changes.map(c => c.file);
|
|
237
|
+
// Build graph
|
|
238
|
+
const allFiles = await getAllRelatedFiles(changedFiles);
|
|
239
|
+
const safeFiles = allFiles.filter(f => !isSensitivePath(f, context.config.blockedPatterns));
|
|
240
|
+
const graph = await gnn.buildCodeGraph(safeFiles, true);
|
|
241
|
+
// Predict impact using GNN propagation
|
|
242
|
+
const impactScores = await gnn.predictImpact(graph, changedFiles, validated.depth);
|
|
243
|
+
// Build file impacts
|
|
244
|
+
const impactedFiles = [];
|
|
245
|
+
for (const [filePath, score] of impactScores) {
|
|
246
|
+
if (score > 0.1) {
|
|
247
|
+
impactedFiles.push({
|
|
248
|
+
filePath,
|
|
249
|
+
impactType: changedFiles.includes(filePath) ? 'direct' :
|
|
250
|
+
score > 0.5 ? 'indirect' : 'transitive',
|
|
251
|
+
requiresChange: changedFiles.includes(filePath) || score > 0.7,
|
|
252
|
+
changesNeeded: getChangesNeeded(filePath, validated.changes),
|
|
253
|
+
risk: score > 0.8 ? 'high' : score > 0.5 ? 'medium' : 'low',
|
|
254
|
+
testsAffected: validated.includeTests
|
|
255
|
+
? getAffectedTests(filePath, graph)
|
|
256
|
+
: [],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Sort by impact
|
|
261
|
+
impactedFiles.sort((a, b) => {
|
|
262
|
+
const aScore = impactScores.get(a.filePath) ?? 0;
|
|
263
|
+
const bScore = impactScores.get(b.filePath) ?? 0;
|
|
264
|
+
return bScore - aScore;
|
|
265
|
+
});
|
|
266
|
+
const result = {
|
|
267
|
+
success: true,
|
|
268
|
+
changes: validated.changes.map(c => ({
|
|
269
|
+
file: c.file,
|
|
270
|
+
type: c.type,
|
|
271
|
+
details: c.details ?? {},
|
|
272
|
+
})),
|
|
273
|
+
impactedFiles,
|
|
274
|
+
summary: {
|
|
275
|
+
directlyAffected: impactedFiles.filter(f => f.impactType === 'direct').length,
|
|
276
|
+
indirectlyAffected: impactedFiles.filter(f => f.impactType !== 'direct').length,
|
|
277
|
+
testsAffected: new Set(impactedFiles.flatMap(f => f.testsAffected)).size,
|
|
278
|
+
totalRisk: impactedFiles.some(f => f.risk === 'high') ? 'high' :
|
|
279
|
+
impactedFiles.some(f => f.risk === 'medium') ? 'medium' : 'low',
|
|
280
|
+
},
|
|
281
|
+
suggestedOrder: getSuggestedOrder(impactedFiles, graph),
|
|
282
|
+
breakingChanges: findBreakingChanges(validated.changes, graph),
|
|
283
|
+
durationMs: Date.now() - startTime,
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
287
|
+
data: result,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
292
|
+
return {
|
|
293
|
+
content: [{
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: JSON.stringify({
|
|
296
|
+
success: false,
|
|
297
|
+
error: errorMessage,
|
|
298
|
+
durationMs: Date.now() - startTime,
|
|
299
|
+
}, null, 2),
|
|
300
|
+
}],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// Split Suggest Tool
|
|
307
|
+
// ============================================================================
|
|
308
|
+
/**
|
|
309
|
+
* MCP Tool: code/split-suggest
|
|
310
|
+
*
|
|
311
|
+
* Suggest optimal code splitting using MinCut algorithm
|
|
312
|
+
*/
|
|
313
|
+
export const splitSuggestTool = {
|
|
314
|
+
name: 'code/split-suggest',
|
|
315
|
+
description: 'Suggest optimal code splitting using MinCut algorithm',
|
|
316
|
+
category: 'code-intelligence',
|
|
317
|
+
version: '3.0.0-alpha.1',
|
|
318
|
+
inputSchema: SplitSuggestInputSchema,
|
|
319
|
+
handler: async (input, context) => {
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
try {
|
|
322
|
+
const validated = SplitSuggestInputSchema.parse(input);
|
|
323
|
+
// Validate path
|
|
324
|
+
const targetPath = validatePath(validated.targetPath, context.config.allowedRoots);
|
|
325
|
+
// Initialize bridges
|
|
326
|
+
const gnn = context.bridges.gnn;
|
|
327
|
+
const mincut = context.bridges.mincut;
|
|
328
|
+
if (!gnn.isInitialized())
|
|
329
|
+
await gnn.initialize();
|
|
330
|
+
if (!mincut.isInitialized())
|
|
331
|
+
await mincut.initialize();
|
|
332
|
+
// Get files
|
|
333
|
+
const files = await getFilesInPath(targetPath);
|
|
334
|
+
const safeFiles = files.filter(f => !isSensitivePath(f, context.config.blockedPatterns));
|
|
335
|
+
// Build graph
|
|
336
|
+
const graph = await gnn.buildCodeGraph(safeFiles, true);
|
|
337
|
+
// Determine number of modules
|
|
338
|
+
const targetModules = validated.targetModules ??
|
|
339
|
+
Math.max(2, Math.ceil(Math.sqrt(graph.nodes.length / 5)));
|
|
340
|
+
// Find optimal cuts
|
|
341
|
+
const partition = await mincut.findOptimalCuts(graph, targetModules, validated.constraints ?? {});
|
|
342
|
+
// Build suggested modules
|
|
343
|
+
const modules = buildSuggestedModules(graph, partition, validated.strategy);
|
|
344
|
+
// Calculate cut edges
|
|
345
|
+
const cutEdges = [];
|
|
346
|
+
for (const edge of graph.edges) {
|
|
347
|
+
const fromPart = partition.get(edge.from);
|
|
348
|
+
const toPart = partition.get(edge.to);
|
|
349
|
+
if (fromPart !== undefined && toPart !== undefined && fromPart !== toPart) {
|
|
350
|
+
cutEdges.push({
|
|
351
|
+
from: edge.from,
|
|
352
|
+
to: edge.to,
|
|
353
|
+
weight: edge.weight,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Calculate quality metrics
|
|
358
|
+
const totalCutWeight = cutEdges.reduce((sum, e) => sum + e.weight, 0);
|
|
359
|
+
const avgCohesion = modules.reduce((sum, m) => sum + m.cohesion, 0) / modules.length;
|
|
360
|
+
const avgCoupling = modules.reduce((sum, m) => sum + m.coupling, 0) / modules.length;
|
|
361
|
+
const sizes = modules.map(m => m.loc);
|
|
362
|
+
const balanceScore = 1 - (Math.max(...sizes) - Math.min(...sizes)) /
|
|
363
|
+
(Math.max(...sizes) + 1);
|
|
364
|
+
const result = {
|
|
365
|
+
success: true,
|
|
366
|
+
targetPath,
|
|
367
|
+
strategy: validated.strategy,
|
|
368
|
+
modules,
|
|
369
|
+
cutEdges,
|
|
370
|
+
quality: {
|
|
371
|
+
totalCutWeight,
|
|
372
|
+
avgCohesion,
|
|
373
|
+
avgCoupling,
|
|
374
|
+
balanceScore,
|
|
375
|
+
},
|
|
376
|
+
migrationSteps: generateMigrationSteps(modules, cutEdges),
|
|
377
|
+
durationMs: Date.now() - startTime,
|
|
378
|
+
};
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
381
|
+
data: result,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
386
|
+
return {
|
|
387
|
+
content: [{
|
|
388
|
+
type: 'text',
|
|
389
|
+
text: JSON.stringify({
|
|
390
|
+
success: false,
|
|
391
|
+
error: errorMessage,
|
|
392
|
+
durationMs: Date.now() - startTime,
|
|
393
|
+
}, null, 2),
|
|
394
|
+
}],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// Learn Patterns Tool
|
|
401
|
+
// ============================================================================
|
|
402
|
+
/**
|
|
403
|
+
* MCP Tool: code/learn-patterns
|
|
404
|
+
*
|
|
405
|
+
* Learn recurring patterns from code changes using SONA
|
|
406
|
+
*/
|
|
407
|
+
export const learnPatternsTool = {
|
|
408
|
+
name: 'code/learn-patterns',
|
|
409
|
+
description: 'Learn recurring patterns from code changes using SONA',
|
|
410
|
+
category: 'code-intelligence',
|
|
411
|
+
version: '3.0.0-alpha.1',
|
|
412
|
+
inputSchema: LearnPatternsInputSchema,
|
|
413
|
+
handler: async (input, context) => {
|
|
414
|
+
const startTime = Date.now();
|
|
415
|
+
try {
|
|
416
|
+
const validated = LearnPatternsInputSchema.parse(input);
|
|
417
|
+
// Analyze git history
|
|
418
|
+
const scope = validated.scope ?? { gitRange: 'HEAD~100..HEAD' };
|
|
419
|
+
const patternTypes = validated.patternTypes ?? [
|
|
420
|
+
'bug_patterns',
|
|
421
|
+
'refactor_patterns',
|
|
422
|
+
];
|
|
423
|
+
// Learn patterns from commits (simplified)
|
|
424
|
+
const patterns = await learnPatternsFromHistory(scope, patternTypes, validated.minOccurrences, context);
|
|
425
|
+
// Generate recommendations
|
|
426
|
+
const recommendations = generateRecommendations(patterns);
|
|
427
|
+
const result = {
|
|
428
|
+
success: true,
|
|
429
|
+
scope,
|
|
430
|
+
patternTypes,
|
|
431
|
+
patterns,
|
|
432
|
+
summary: {
|
|
433
|
+
commitsAnalyzed: 100, // Simplified
|
|
434
|
+
filesAnalyzed: patterns.reduce((sum, p) => sum + p.files.length, 0),
|
|
435
|
+
patternsFound: patterns.length,
|
|
436
|
+
byType: patternTypes.reduce((acc, type) => {
|
|
437
|
+
acc[type] = patterns.filter(p => p.type === type).length;
|
|
438
|
+
return acc;
|
|
439
|
+
}, {}),
|
|
440
|
+
},
|
|
441
|
+
recommendations,
|
|
442
|
+
durationMs: Date.now() - startTime,
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
446
|
+
data: result,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: JSON.stringify({
|
|
455
|
+
success: false,
|
|
456
|
+
error: errorMessage,
|
|
457
|
+
durationMs: Date.now() - startTime,
|
|
458
|
+
}, null, 2),
|
|
459
|
+
}],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// Helper Functions
|
|
466
|
+
// ============================================================================
|
|
467
|
+
async function performSemanticSearch(_query, _paths, _searchType, topK, _languages, _excludeTests, _context) {
|
|
468
|
+
// Simplified implementation
|
|
469
|
+
const results = [];
|
|
470
|
+
// In production, would use vector index
|
|
471
|
+
return results.slice(0, topK);
|
|
472
|
+
}
|
|
473
|
+
async function getFilesInPath(_rootPath) {
|
|
474
|
+
// Simplified - in production would use glob
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
async function getAllRelatedFiles(changedFiles) {
|
|
478
|
+
// Simplified - would traverse dependencies
|
|
479
|
+
return changedFiles;
|
|
480
|
+
}
|
|
481
|
+
function detectLayerViolations(graph, layers) {
|
|
482
|
+
const violations = [];
|
|
483
|
+
if (!layers)
|
|
484
|
+
return violations;
|
|
485
|
+
// Build layer lookup
|
|
486
|
+
const nodeLayer = new Map();
|
|
487
|
+
for (const [layer, patterns] of Object.entries(layers)) {
|
|
488
|
+
for (const pattern of patterns) {
|
|
489
|
+
for (const node of graph.nodes) {
|
|
490
|
+
if (node.id.includes(pattern)) {
|
|
491
|
+
nodeLayer.set(node.id, layer);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Check edges for violations
|
|
497
|
+
for (const edge of graph.edges) {
|
|
498
|
+
const fromLayer = nodeLayer.get(edge.from);
|
|
499
|
+
const toLayer = nodeLayer.get(edge.to);
|
|
500
|
+
if (fromLayer && toLayer && fromLayer !== toLayer) {
|
|
501
|
+
// Simplified check - in production would check layer order
|
|
502
|
+
violations.push({
|
|
503
|
+
source: edge.from,
|
|
504
|
+
target: edge.to,
|
|
505
|
+
sourceLayer: fromLayer,
|
|
506
|
+
targetLayer: toLayer,
|
|
507
|
+
violationType: 'cross',
|
|
508
|
+
severity: 'medium',
|
|
509
|
+
suggestedFix: `Move ${edge.from} or ${edge.to} to appropriate layer`,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return violations;
|
|
514
|
+
}
|
|
515
|
+
function detectCircularDeps(graph) {
|
|
516
|
+
const cycles = [];
|
|
517
|
+
// Build adjacency list
|
|
518
|
+
const adj = new Map();
|
|
519
|
+
for (const node of graph.nodes) {
|
|
520
|
+
adj.set(node.id, []);
|
|
521
|
+
}
|
|
522
|
+
for (const edge of graph.edges) {
|
|
523
|
+
adj.get(edge.from)?.push(edge.to);
|
|
524
|
+
}
|
|
525
|
+
// DFS for cycle detection
|
|
526
|
+
const visited = new Set();
|
|
527
|
+
const recStack = new Set();
|
|
528
|
+
const findCycle = (node, path) => {
|
|
529
|
+
visited.add(node);
|
|
530
|
+
recStack.add(node);
|
|
531
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
532
|
+
if (recStack.has(neighbor)) {
|
|
533
|
+
// Found cycle
|
|
534
|
+
const cycleStart = path.indexOf(neighbor);
|
|
535
|
+
if (cycleStart >= 0) {
|
|
536
|
+
cycles.push({
|
|
537
|
+
cycle: [...path.slice(cycleStart), neighbor],
|
|
538
|
+
length: path.length - cycleStart + 1,
|
|
539
|
+
severity: path.length - cycleStart > 3 ? 'high' : 'medium',
|
|
540
|
+
suggestedBreakPoint: neighbor,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (!visited.has(neighbor)) {
|
|
545
|
+
findCycle(neighbor, [...path, neighbor]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
recStack.delete(node);
|
|
549
|
+
};
|
|
550
|
+
for (const node of graph.nodes) {
|
|
551
|
+
if (!visited.has(node.id)) {
|
|
552
|
+
findCycle(node.id, [node.id]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return cycles;
|
|
556
|
+
}
|
|
557
|
+
function calculateCouplingMetrics(graph) {
|
|
558
|
+
const metrics = [];
|
|
559
|
+
for (const node of graph.nodes) {
|
|
560
|
+
const afferent = graph.edges.filter(e => e.to === node.id).length;
|
|
561
|
+
const efferent = graph.edges.filter(e => e.from === node.id).length;
|
|
562
|
+
const instability = (afferent + efferent) > 0
|
|
563
|
+
? efferent / (afferent + efferent)
|
|
564
|
+
: 0;
|
|
565
|
+
metrics.push({
|
|
566
|
+
componentId: node.id,
|
|
567
|
+
afferentCoupling: afferent,
|
|
568
|
+
efferentCoupling: efferent,
|
|
569
|
+
instability,
|
|
570
|
+
abstractness: 0.5, // Simplified
|
|
571
|
+
distanceFromMain: Math.abs(instability - 0.5),
|
|
572
|
+
inZoneOfPain: instability < 0.3 && false, // Simplified
|
|
573
|
+
inZoneOfUselessness: instability > 0.7 && false, // Simplified
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return metrics;
|
|
577
|
+
}
|
|
578
|
+
function calculateCohesionMetrics(_graph) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
function findDeadCode(graph) {
|
|
582
|
+
const deadCode = [];
|
|
583
|
+
// Find nodes with no incoming edges and not exported
|
|
584
|
+
const hasIncoming = new Set(graph.edges.map(e => e.to));
|
|
585
|
+
for (const node of graph.nodes) {
|
|
586
|
+
if (!hasIncoming.has(node.id) && node.type === 'function') {
|
|
587
|
+
deadCode.push({
|
|
588
|
+
filePath: node.id,
|
|
589
|
+
symbol: node.label,
|
|
590
|
+
symbolType: 'function',
|
|
591
|
+
lineNumber: 1,
|
|
592
|
+
confidence: 0.7,
|
|
593
|
+
reason: 'No references found',
|
|
594
|
+
isExported: false,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return deadCode;
|
|
599
|
+
}
|
|
600
|
+
function analyzeAPISurface(_graph) {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
async function detectDrift(_graph, _baseline) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
function countModules(graph) {
|
|
607
|
+
const dirs = new Set();
|
|
608
|
+
for (const node of graph.nodes) {
|
|
609
|
+
const parts = node.id.split('/');
|
|
610
|
+
if (parts.length > 1) {
|
|
611
|
+
dirs.add(parts.slice(0, -1).join('/'));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return dirs.size;
|
|
615
|
+
}
|
|
616
|
+
function calculateHealthScore(graph) {
|
|
617
|
+
// Simplified scoring
|
|
618
|
+
const nodeCount = graph.nodes.length;
|
|
619
|
+
const edgeCount = graph.edges.length;
|
|
620
|
+
if (nodeCount === 0)
|
|
621
|
+
return 100;
|
|
622
|
+
const avgDegree = edgeCount / nodeCount;
|
|
623
|
+
const idealDegree = 3;
|
|
624
|
+
return Math.max(0, 100 - Math.abs(avgDegree - idealDegree) * 10);
|
|
625
|
+
}
|
|
626
|
+
function getChangesNeeded(filePath, changes) {
|
|
627
|
+
const changesNeeded = [];
|
|
628
|
+
for (const change of changes) {
|
|
629
|
+
if (change.file === filePath) {
|
|
630
|
+
changesNeeded.push(`Apply ${change.type}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return changesNeeded;
|
|
634
|
+
}
|
|
635
|
+
function getAffectedTests(filePath, graph) {
|
|
636
|
+
const tests = [];
|
|
637
|
+
for (const edge of graph.edges) {
|
|
638
|
+
if (edge.from === filePath && edge.to.includes('.test')) {
|
|
639
|
+
tests.push(edge.to);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return tests;
|
|
643
|
+
}
|
|
644
|
+
function getSuggestedOrder(impactedFiles, _graph) {
|
|
645
|
+
// Order by dependencies
|
|
646
|
+
return impactedFiles
|
|
647
|
+
.filter(f => f.requiresChange)
|
|
648
|
+
.sort((a, b) => {
|
|
649
|
+
const aRisk = a.risk === 'high' ? 3 : a.risk === 'medium' ? 2 : 1;
|
|
650
|
+
const bRisk = b.risk === 'high' ? 3 : b.risk === 'medium' ? 2 : 1;
|
|
651
|
+
return bRisk - aRisk;
|
|
652
|
+
})
|
|
653
|
+
.map(f => f.filePath);
|
|
654
|
+
}
|
|
655
|
+
function findBreakingChanges(changes, graph) {
|
|
656
|
+
const breakingChanges = [];
|
|
657
|
+
for (const change of changes) {
|
|
658
|
+
if (change.type === 'delete') {
|
|
659
|
+
const dependents = graph.edges.filter(e => e.to === change.file);
|
|
660
|
+
if (dependents.length > 0) {
|
|
661
|
+
breakingChanges.push(`Deleting ${change.file} breaks ${dependents.length} dependents`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return breakingChanges;
|
|
666
|
+
}
|
|
667
|
+
function buildSuggestedModules(graph, partition, _strategy) {
|
|
668
|
+
const modules = [];
|
|
669
|
+
const partitionGroups = new Map();
|
|
670
|
+
for (const [nodeId, partNum] of partition) {
|
|
671
|
+
if (!partitionGroups.has(partNum)) {
|
|
672
|
+
partitionGroups.set(partNum, []);
|
|
673
|
+
}
|
|
674
|
+
partitionGroups.get(partNum)?.push(nodeId);
|
|
675
|
+
}
|
|
676
|
+
for (const [partNum, files] of partitionGroups) {
|
|
677
|
+
// Calculate cohesion (internal edges / possible internal edges)
|
|
678
|
+
const internalEdges = graph.edges.filter(e => partition.get(e.from) === partNum && partition.get(e.to) === partNum).length;
|
|
679
|
+
const possibleEdges = files.length * (files.length - 1);
|
|
680
|
+
const cohesion = possibleEdges > 0 ? internalEdges / possibleEdges : 1;
|
|
681
|
+
// Calculate coupling (external edges)
|
|
682
|
+
const externalEdges = graph.edges.filter(e => (partition.get(e.from) === partNum) !== (partition.get(e.to) === partNum)).length;
|
|
683
|
+
const coupling = externalEdges / Math.max(files.length, 1);
|
|
684
|
+
// Get dependencies on other modules
|
|
685
|
+
const dependencies = new Set();
|
|
686
|
+
for (const edge of graph.edges) {
|
|
687
|
+
if (partition.get(edge.from) === partNum && partition.get(edge.to) !== partNum) {
|
|
688
|
+
const depModule = partition.get(edge.to);
|
|
689
|
+
if (depModule !== undefined) {
|
|
690
|
+
dependencies.add(`module-${depModule}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
modules.push({
|
|
695
|
+
name: `module-${partNum}`,
|
|
696
|
+
files,
|
|
697
|
+
loc: files.length * 100, // Simplified
|
|
698
|
+
cohesion,
|
|
699
|
+
coupling,
|
|
700
|
+
publicApi: [], // Simplified
|
|
701
|
+
dependencies: Array.from(dependencies),
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return modules;
|
|
705
|
+
}
|
|
706
|
+
function generateMigrationSteps(modules, cutEdges) {
|
|
707
|
+
const steps = [];
|
|
708
|
+
steps.push(`1. Create ${modules.length} new module directories`);
|
|
709
|
+
steps.push(`2. Move files to their respective modules`);
|
|
710
|
+
steps.push(`3. Update ${cutEdges.length} cross-module imports`);
|
|
711
|
+
steps.push(`4. Define public APIs for each module`);
|
|
712
|
+
steps.push(`5. Run tests to verify no regressions`);
|
|
713
|
+
return steps;
|
|
714
|
+
}
|
|
715
|
+
async function learnPatternsFromHistory(_scope, _patternTypes, _minOccurrences, _context) {
|
|
716
|
+
// Simplified - in production would analyze git history
|
|
717
|
+
return [
|
|
718
|
+
{
|
|
719
|
+
id: 'pattern-1',
|
|
720
|
+
type: 'refactor_patterns',
|
|
721
|
+
description: 'Convert callbacks to async/await',
|
|
722
|
+
codeBefore: 'function(callback) { ... }',
|
|
723
|
+
codeAfter: 'async function() { ... }',
|
|
724
|
+
occurrences: 5,
|
|
725
|
+
authors: ['developer1'],
|
|
726
|
+
files: ['src/utils.ts'],
|
|
727
|
+
confidence: 0.85,
|
|
728
|
+
impact: 'positive',
|
|
729
|
+
suggestedAction: 'Consider modernizing callback-based code to async/await',
|
|
730
|
+
},
|
|
731
|
+
];
|
|
732
|
+
}
|
|
733
|
+
function generateRecommendations(patterns) {
|
|
734
|
+
const recommendations = [];
|
|
735
|
+
for (const pattern of patterns) {
|
|
736
|
+
if (pattern.suggestedAction) {
|
|
737
|
+
recommendations.push(pattern.suggestedAction);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return recommendations;
|
|
741
|
+
}
|
|
742
|
+
// ============================================================================
|
|
743
|
+
// Tool Registry
|
|
744
|
+
// ============================================================================
|
|
745
|
+
/**
|
|
746
|
+
* All Code Intelligence MCP Tools
|
|
747
|
+
*/
|
|
748
|
+
export const codeIntelligenceTools = [
|
|
749
|
+
semanticSearchTool,
|
|
750
|
+
architectureAnalyzeTool,
|
|
751
|
+
refactorImpactTool,
|
|
752
|
+
splitSuggestTool,
|
|
753
|
+
learnPatternsTool,
|
|
754
|
+
];
|
|
755
|
+
/**
|
|
756
|
+
* Tool name to handler map
|
|
757
|
+
*/
|
|
758
|
+
export const toolHandlers = new Map([
|
|
759
|
+
['code/semantic-search', semanticSearchTool.handler],
|
|
760
|
+
['code/architecture-analyze', architectureAnalyzeTool.handler],
|
|
761
|
+
['code/refactor-impact', refactorImpactTool.handler],
|
|
762
|
+
['code/split-suggest', splitSuggestTool.handler],
|
|
763
|
+
['code/learn-patterns', learnPatternsTool.handler],
|
|
764
|
+
]);
|
|
765
|
+
/**
|
|
766
|
+
* Create tool context with bridges
|
|
767
|
+
*/
|
|
768
|
+
export function createToolContext(config) {
|
|
769
|
+
const store = new Map();
|
|
770
|
+
const defaultBlockedPatterns = [
|
|
771
|
+
/\.env$/,
|
|
772
|
+
/\.git\/config$/,
|
|
773
|
+
/credentials/i,
|
|
774
|
+
/secrets?\./i,
|
|
775
|
+
/\.pem$/,
|
|
776
|
+
/\.key$/,
|
|
777
|
+
/id_rsa/i,
|
|
778
|
+
];
|
|
779
|
+
return {
|
|
780
|
+
get: (key) => store.get(key),
|
|
781
|
+
set: (key, value) => { store.set(key, value); },
|
|
782
|
+
bridges: {
|
|
783
|
+
gnn: createGNNBridge(),
|
|
784
|
+
mincut: createMinCutBridge(),
|
|
785
|
+
},
|
|
786
|
+
config: {
|
|
787
|
+
allowedRoots: config?.allowedRoots ?? ['.'],
|
|
788
|
+
blockedPatterns: config?.blockedPatterns ?? defaultBlockedPatterns,
|
|
789
|
+
maskSecrets: config?.maskSecrets ?? true,
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
export default codeIntelligenceTools;
|
|
794
|
+
//# sourceMappingURL=mcp-tools.js.map
|