code-graph-context 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/constants.js +21 -0
- package/dist/core/config/nestjs-framework-schema.js +2 -1
- package/dist/core/config/schema.js +2 -1
- package/dist/core/embeddings/natural-language-to-cypher.service.js +12 -13
- package/dist/core/parsers/parser-factory.js +3 -2
- package/dist/core/parsers/typescript-parser.js +129 -39
- package/dist/mcp/constants.js +19 -2
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/impact-analysis.tool.js +253 -0
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +127 -42
- package/dist/mcp/tools/search-codebase.tool.js +4 -10
- package/dist/mcp/utils.js +4 -17
- package/dist/storage/neo4j/neo4j.service.js +201 -6
- package/dist/utils/file-utils.js +20 -0
- package/package.json +2 -1
package/dist/mcp/services.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { Neo4jService } from '../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
|
|
8
8
|
import { FILE_PATHS, LOG_CONFIG } from './constants.js';
|
|
9
9
|
import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
|
|
10
10
|
import { debugLog } from './utils.js';
|
|
@@ -15,141 +15,62 @@ export const initializeServices = async () => {
|
|
|
15
15
|
await Promise.all([initializeNeo4jSchema(), initializeNaturalLanguageService()]);
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Dynamically discover schema from the actual graph contents.
|
|
19
|
+
* This is framework-agnostic - it discovers what's actually in the graph.
|
|
19
20
|
*/
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
57
|
-
VendorClient: {
|
|
58
|
-
description: 'External service integration clients',
|
|
59
|
-
purpose: 'Interface with third-party APIs and services',
|
|
60
|
-
commonProperties: ['name', 'filePath'],
|
|
61
|
-
exampleQuery: 'MATCH (v:VendorClient)<-[:INJECTS]-(s) RETURN v.name, collect(s.name) as usedBy',
|
|
62
|
-
},
|
|
63
|
-
RouteDefinition: {
|
|
64
|
-
description: 'Explicit route definitions from route files. CRITICAL: Individual route details (method, path, authenticated, handler, controllerName) are stored in the "context" property as a JSON string.',
|
|
65
|
-
purpose: 'Map HTTP paths and methods to controller handlers',
|
|
66
|
-
commonProperties: ['name', 'context', 'filePath', 'sourceCode'],
|
|
67
|
-
contextStructure: 'The context property contains JSON with structure: {"routes": [{"method": "POST", "path": "/v1/endpoint", "controllerName": "SomeController", "handler": "methodName", "authenticated": true}]}',
|
|
68
|
-
parsingInstructions: 'To get individual routes: (1) Parse JSON with apoc.convert.fromJsonMap(rd.context) (2) UNWIND the routes array (3) Access route.method, route.path, route.handler, route.authenticated, route.controllerName',
|
|
69
|
-
exampleQuery: 'MATCH (rd:RouteDefinition) WITH rd, apoc.convert.fromJsonMap(rd.context) AS ctx UNWIND ctx.routes AS route RETURN route.method, route.path, route.controllerName, route.handler, route.authenticated ORDER BY route.path',
|
|
70
|
-
},
|
|
71
|
-
HttpEndpoint: {
|
|
72
|
-
description: 'Methods that handle HTTP requests',
|
|
73
|
-
purpose: 'Process incoming HTTP requests and return responses',
|
|
74
|
-
commonProperties: ['name', 'filePath', 'sourceCode'],
|
|
75
|
-
exampleQuery: 'MATCH (e:HttpEndpoint)<-[r:ROUTES_TO_HANDLER]-(rd) WHERE apoc.convert.fromJsonMap(r.context).authenticated = true RETURN e.name, apoc.convert.fromJsonMap(r.context).path as path',
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
relationships: {
|
|
79
|
-
INJECTS: {
|
|
80
|
-
description: 'Dependency injection relationship from @Injectable decorator',
|
|
81
|
-
direction: 'OUTGOING',
|
|
82
|
-
example: 'Controller -[:INJECTS]-> Service',
|
|
83
|
-
commonPatterns: ['Controller -> Service', 'Service -> Repository', 'Service -> VendorClient'],
|
|
84
|
-
},
|
|
85
|
-
USES_DAL: {
|
|
86
|
-
description: 'Repository uses Data Access Layer for database operations',
|
|
87
|
-
direction: 'OUTGOING',
|
|
88
|
-
example: 'Repository -[:USES_DAL]-> DAL',
|
|
89
|
-
commonPatterns: ['Repository -> DAL'],
|
|
90
|
-
},
|
|
91
|
-
ROUTES_TO: {
|
|
92
|
-
description: 'Route definition points to a Controller',
|
|
93
|
-
direction: 'OUTGOING',
|
|
94
|
-
example: 'RouteDefinition -[:ROUTES_TO]-> Controller',
|
|
95
|
-
commonPatterns: ['RouteDefinition -> Controller'],
|
|
96
|
-
},
|
|
97
|
-
ROUTES_TO_HANDLER: {
|
|
98
|
-
description: 'Route definition points to a specific handler method',
|
|
99
|
-
direction: 'OUTGOING',
|
|
100
|
-
example: 'RouteDefinition -[:ROUTES_TO_HANDLER]-> HttpEndpoint',
|
|
101
|
-
contextProperties: ['path', 'method', 'authenticated', 'handler', 'controllerName'],
|
|
102
|
-
contextNote: 'IMPORTANT: context is stored as a JSON string. Access properties using apoc.convert.fromJsonMap(r.context).propertyName',
|
|
103
|
-
commonPatterns: ['RouteDefinition -> HttpEndpoint (Method)'],
|
|
104
|
-
},
|
|
105
|
-
PROTECTED_BY: {
|
|
106
|
-
description: 'Controller is protected by a PermissionManager',
|
|
107
|
-
direction: 'OUTGOING',
|
|
108
|
-
example: 'Controller -[:PROTECTED_BY]-> PermissionManager',
|
|
109
|
-
commonPatterns: ['Controller -> PermissionManager'],
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
commonQueryPatterns: [
|
|
113
|
-
{
|
|
114
|
-
intent: 'Find all HTTP endpoints',
|
|
115
|
-
query: 'MATCH (e:HttpEndpoint) RETURN e.name, e.filePath',
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
intent: 'Find service dependency chain',
|
|
119
|
-
query: 'MATCH path = (c:Controller)-[:INJECTS*1..3]->(s) RETURN [n in nodes(path) | n.name] as chain',
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
intent: 'Find all authenticated routes',
|
|
123
|
-
query: 'MATCH (rd:RouteDefinition)-[r:ROUTES_TO_HANDLER]->(m) WHERE apoc.convert.fromJsonMap(r.context).authenticated = true RETURN apoc.convert.fromJsonMap(r.context).path as path, apoc.convert.fromJsonMap(r.context).method as method, m.name',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
intent: 'Find controllers without permission managers',
|
|
127
|
-
query: 'MATCH (c:Controller) WHERE NOT (c)-[:PROTECTED_BY]->(:PermissionManager) RETURN c.name',
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
intent: 'Find what services a controller uses',
|
|
131
|
-
query: 'MATCH (c:Controller {name: $controllerName})-[:INJECTS]->(s:Service) RETURN s.name',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
intent: 'Find complete execution path from controller to database',
|
|
135
|
-
query: 'MATCH path = (c:Controller)-[:INJECTS*1..3]->(r:Repository)-[:USES_DAL]->(d:DAL) WHERE c.name = $controllerName RETURN [n in nodes(path) | n.name] as executionPath',
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
};
|
|
21
|
+
const discoverSchemaFromGraph = async (neo4jService) => {
|
|
22
|
+
try {
|
|
23
|
+
// Discover actual node types, relationships, and patterns from the graph
|
|
24
|
+
const [nodeTypes, relationshipTypes, semanticTypes, commonPatterns] = await Promise.all([
|
|
25
|
+
neo4jService.run(QUERIES.DISCOVER_NODE_TYPES),
|
|
26
|
+
neo4jService.run(QUERIES.DISCOVER_RELATIONSHIP_TYPES),
|
|
27
|
+
neo4jService.run(QUERIES.DISCOVER_SEMANTIC_TYPES),
|
|
28
|
+
neo4jService.run(QUERIES.DISCOVER_COMMON_PATTERNS),
|
|
29
|
+
]);
|
|
30
|
+
return {
|
|
31
|
+
nodeTypes: nodeTypes.map((r) => ({
|
|
32
|
+
label: r.label,
|
|
33
|
+
count: typeof r.nodeCount === 'object' ? r.nodeCount.toNumber() : r.nodeCount,
|
|
34
|
+
properties: r.sampleProperties || [],
|
|
35
|
+
})),
|
|
36
|
+
relationshipTypes: relationshipTypes.map((r) => ({
|
|
37
|
+
type: r.relationshipType,
|
|
38
|
+
count: typeof r.relCount === 'object' ? r.relCount.toNumber() : r.relCount,
|
|
39
|
+
connections: r.connections || [],
|
|
40
|
+
})),
|
|
41
|
+
semanticTypes: semanticTypes.map((r) => ({
|
|
42
|
+
type: r.semanticType,
|
|
43
|
+
count: typeof r.count === 'object' ? r.count.toNumber() : r.count,
|
|
44
|
+
})),
|
|
45
|
+
commonPatterns: commonPatterns.map((r) => ({
|
|
46
|
+
from: r.fromType,
|
|
47
|
+
relationship: r.relType,
|
|
48
|
+
to: r.toType,
|
|
49
|
+
count: typeof r.count === 'object' ? r.count.toNumber() : r.count,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
await debugLog('Failed to discover schema from graph', error);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
140
57
|
};
|
|
141
58
|
/**
|
|
142
|
-
* Initialize Neo4j schema by fetching and
|
|
59
|
+
* Initialize Neo4j schema by fetching from APOC and discovering actual graph structure
|
|
143
60
|
*/
|
|
144
61
|
const initializeNeo4jSchema = async () => {
|
|
145
62
|
try {
|
|
146
63
|
const neo4jService = new Neo4jService();
|
|
147
64
|
const rawSchema = await neo4jService.getSchema();
|
|
148
|
-
//
|
|
149
|
-
const
|
|
65
|
+
// Dynamically discover what's actually in the graph
|
|
66
|
+
const discoveredSchema = await discoverSchemaFromGraph(neo4jService);
|
|
67
|
+
const schema = {
|
|
68
|
+
rawSchema,
|
|
69
|
+
discoveredSchema,
|
|
70
|
+
};
|
|
150
71
|
const schemaPath = join(process.cwd(), FILE_PATHS.schemaOutput);
|
|
151
|
-
await fs.writeFile(schemaPath, JSON.stringify(
|
|
152
|
-
await debugLog('Neo4j schema cached successfully
|
|
72
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema, null, LOG_CONFIG.jsonIndentation));
|
|
73
|
+
await debugLog('Neo4j schema cached successfully', { schemaPath });
|
|
153
74
|
}
|
|
154
75
|
catch (error) {
|
|
155
76
|
await debugLog('Failed to initialize Neo4j schema', error);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Analysis Tool
|
|
3
|
+
* Analyzes what would be affected if a node is modified
|
|
4
|
+
* Reuses cross-file edge pattern from incremental parsing
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
|
|
8
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
9
|
+
import { createErrorResponse, createSuccessResponse, debugLog } from '../utils.js';
|
|
10
|
+
// Default relationship weights for core AST relationships
|
|
11
|
+
const DEFAULT_RELATIONSHIP_WEIGHTS = {
|
|
12
|
+
// Critical - inheritance/interface contracts
|
|
13
|
+
EXTENDS: 0.95,
|
|
14
|
+
IMPLEMENTS: 0.95,
|
|
15
|
+
// High - direct code dependencies
|
|
16
|
+
CALLS: 0.75,
|
|
17
|
+
HAS_MEMBER: 0.65,
|
|
18
|
+
TYPED_AS: 0.6,
|
|
19
|
+
// Medium - module dependencies
|
|
20
|
+
IMPORTS: 0.5,
|
|
21
|
+
EXPORTS: 0.5,
|
|
22
|
+
// Lower - structural
|
|
23
|
+
CONTAINS: 0.3,
|
|
24
|
+
HAS_PARAMETER: 0.3,
|
|
25
|
+
DECORATED_WITH: 0.4,
|
|
26
|
+
};
|
|
27
|
+
// Schema for framework-specific configuration
|
|
28
|
+
const FrameworkConfigSchema = z.object({
|
|
29
|
+
relationshipWeights: z.record(z.string(), z.number().min(0).max(1)).optional(),
|
|
30
|
+
highRiskTypes: z.array(z.string()).optional(),
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
export const createImpactAnalysisTool = (server) => {
|
|
34
|
+
server.registerTool(TOOL_NAMES.impactAnalysis, {
|
|
35
|
+
title: TOOL_METADATA[TOOL_NAMES.impactAnalysis].title,
|
|
36
|
+
description: TOOL_METADATA[TOOL_NAMES.impactAnalysis].description,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
nodeId: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('The node ID to analyze impact for (from search_codebase or traverse_from_node results)'),
|
|
42
|
+
filePath: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Alternatively, provide a file path to analyze all exports from that file'),
|
|
46
|
+
maxDepth: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(6)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Maximum depth to traverse for transitive dependents (default: 4)')
|
|
53
|
+
.default(4),
|
|
54
|
+
frameworkConfig: FrameworkConfigSchema.optional().describe('Framework-specific configuration for risk scoring. Includes relationshipWeights (e.g., {"INJECTS": 0.9}), highRiskTypes (e.g., ["Controller", "Service"]), and optional name.'),
|
|
55
|
+
},
|
|
56
|
+
}, async ({ nodeId, filePath, maxDepth = 4, frameworkConfig }) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!nodeId && !filePath) {
|
|
59
|
+
return createErrorResponse('Either nodeId or filePath must be provided');
|
|
60
|
+
}
|
|
61
|
+
await debugLog('Impact analysis started', { nodeId, filePath, maxDepth, frameworkConfig });
|
|
62
|
+
const neo4jService = new Neo4jService();
|
|
63
|
+
// Merge default weights with framework-specific weights
|
|
64
|
+
const weights = { ...DEFAULT_RELATIONSHIP_WEIGHTS, ...frameworkConfig?.relationshipWeights };
|
|
65
|
+
const highRiskTypes = new Set(frameworkConfig?.highRiskTypes ?? []);
|
|
66
|
+
let targetInfo;
|
|
67
|
+
let directDependents;
|
|
68
|
+
if (nodeId) {
|
|
69
|
+
// Get target node info
|
|
70
|
+
const targetResult = await neo4jService.run(QUERIES.GET_NODE_BY_ID, { nodeId });
|
|
71
|
+
if (targetResult.length === 0) {
|
|
72
|
+
return createErrorResponse(`Node with ID "${nodeId}" not found`);
|
|
73
|
+
}
|
|
74
|
+
const target = targetResult[0];
|
|
75
|
+
targetInfo = {
|
|
76
|
+
id: target.id,
|
|
77
|
+
name: target.name ?? 'Unknown',
|
|
78
|
+
type: target.semanticType ?? target.coreType ?? target.labels?.[0] ?? 'Unknown',
|
|
79
|
+
filePath: target.filePath ?? '',
|
|
80
|
+
};
|
|
81
|
+
// Get direct dependents using cross-file edge pattern
|
|
82
|
+
const directResult = await neo4jService.run(QUERIES.GET_NODE_IMPACT, { nodeId });
|
|
83
|
+
directDependents = normalizeDependents(directResult);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// File-based analysis
|
|
87
|
+
targetInfo = {
|
|
88
|
+
id: filePath,
|
|
89
|
+
name: filePath.split('/').pop() ?? filePath,
|
|
90
|
+
type: 'SourceFile',
|
|
91
|
+
filePath: filePath,
|
|
92
|
+
};
|
|
93
|
+
// Get all external dependents on this file
|
|
94
|
+
const fileResult = await neo4jService.run(QUERIES.GET_FILE_IMPACT, { filePath });
|
|
95
|
+
directDependents = normalizeDependents(fileResult);
|
|
96
|
+
}
|
|
97
|
+
// Get transitive dependents if nodeId provided
|
|
98
|
+
let transitiveDependents = [];
|
|
99
|
+
if (nodeId && maxDepth > 1) {
|
|
100
|
+
const transitiveResult = await neo4jService.run(QUERIES.GET_TRANSITIVE_DEPENDENTS(maxDepth), { nodeId });
|
|
101
|
+
transitiveDependents = normalizeTransitiveDependents(transitiveResult);
|
|
102
|
+
// Filter out direct dependents from transitive
|
|
103
|
+
const directIds = new Set(directDependents.map((d) => d.nodeId));
|
|
104
|
+
transitiveDependents = transitiveDependents.filter((d) => !directIds.has(d.nodeId));
|
|
105
|
+
}
|
|
106
|
+
// Calculate risk score
|
|
107
|
+
const riskScore = calculateRiskScore(directDependents, transitiveDependents, weights, highRiskTypes);
|
|
108
|
+
const riskLevel = getRiskLevel(riskScore);
|
|
109
|
+
// Group dependents by type
|
|
110
|
+
const directByType = groupByType(directDependents);
|
|
111
|
+
const directByRelationship = groupByRelationship(directDependents);
|
|
112
|
+
const transitiveByType = groupByType(transitiveDependents);
|
|
113
|
+
// Get affected files
|
|
114
|
+
const affectedFiles = getAffectedFiles([...directDependents, ...transitiveDependents]);
|
|
115
|
+
// Find critical paths (high-weight relationships)
|
|
116
|
+
const criticalPaths = findCriticalPaths(directDependents, targetInfo, weights);
|
|
117
|
+
// Build summary
|
|
118
|
+
const summary = buildSummary(targetInfo, directDependents.length, transitiveDependents.length, affectedFiles.length, riskLevel);
|
|
119
|
+
const result = {
|
|
120
|
+
target: targetInfo,
|
|
121
|
+
riskLevel,
|
|
122
|
+
riskScore: Math.round(riskScore * 100) / 100,
|
|
123
|
+
summary,
|
|
124
|
+
directDependents: {
|
|
125
|
+
count: directDependents.length,
|
|
126
|
+
byType: directByType,
|
|
127
|
+
byRelationship: directByRelationship,
|
|
128
|
+
},
|
|
129
|
+
transitiveDependents: {
|
|
130
|
+
count: transitiveDependents.length,
|
|
131
|
+
maxDepth: getMaxDepth(transitiveDependents),
|
|
132
|
+
byType: transitiveByType,
|
|
133
|
+
},
|
|
134
|
+
affectedFiles,
|
|
135
|
+
criticalPaths,
|
|
136
|
+
};
|
|
137
|
+
await debugLog('Impact analysis complete', {
|
|
138
|
+
nodeId: nodeId ?? filePath,
|
|
139
|
+
riskLevel,
|
|
140
|
+
directCount: directDependents.length,
|
|
141
|
+
transitiveCount: transitiveDependents.length,
|
|
142
|
+
});
|
|
143
|
+
return createSuccessResponse(JSON.stringify(result, null, 2));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Impact analysis error:', error);
|
|
147
|
+
await debugLog('Impact analysis error', { nodeId, filePath, error });
|
|
148
|
+
return createErrorResponse(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
// Helper functions
|
|
153
|
+
const normalizeDependents = (results) => {
|
|
154
|
+
return results.map((r) => ({
|
|
155
|
+
nodeId: r.nodeId,
|
|
156
|
+
name: r.name ?? 'Unknown',
|
|
157
|
+
labels: r.labels ?? [],
|
|
158
|
+
semanticType: r.semanticType,
|
|
159
|
+
coreType: r.coreType,
|
|
160
|
+
filePath: r.filePath ?? '',
|
|
161
|
+
relationshipType: r.relationshipType ?? 'UNKNOWN',
|
|
162
|
+
weight: typeof r.weight === 'object'
|
|
163
|
+
? r.weight.toNumber()
|
|
164
|
+
: (r.weight ?? 0.5),
|
|
165
|
+
}));
|
|
166
|
+
};
|
|
167
|
+
const normalizeTransitiveDependents = (results) => {
|
|
168
|
+
return results.map((r) => ({
|
|
169
|
+
nodeId: r.nodeId,
|
|
170
|
+
name: r.name ?? 'Unknown',
|
|
171
|
+
labels: r.labels ?? [],
|
|
172
|
+
semanticType: r.semanticType,
|
|
173
|
+
coreType: r.coreType,
|
|
174
|
+
filePath: r.filePath ?? '',
|
|
175
|
+
relationshipType: r.relationshipPath?.[0] ?? 'UNKNOWN',
|
|
176
|
+
weight: 0.5,
|
|
177
|
+
depth: typeof r.depth === 'object' ? r.depth.toNumber() : r.depth,
|
|
178
|
+
relationshipPath: r.relationshipPath,
|
|
179
|
+
}));
|
|
180
|
+
};
|
|
181
|
+
const calculateRiskScore = (directDependents, transitiveDependents, weights, highRiskTypes) => {
|
|
182
|
+
if (directDependents.length === 0)
|
|
183
|
+
return 0;
|
|
184
|
+
let score = 0;
|
|
185
|
+
// Factor 1: Number of direct dependents (logarithmic, max 0.3)
|
|
186
|
+
score += Math.min(Math.log10(directDependents.length + 1) / 2, 0.3);
|
|
187
|
+
// Factor 2: Average relationship weight of direct deps (max 0.3)
|
|
188
|
+
const avgWeight = directDependents.reduce((sum, d) => sum + (weights[d.relationshipType] ?? d.weight), 0) / directDependents.length;
|
|
189
|
+
score += avgWeight * 0.3;
|
|
190
|
+
// Factor 3: High-risk types affected (max 0.2)
|
|
191
|
+
const highRiskCount = directDependents.filter((d) => highRiskTypes.has(d.semanticType ?? '') || highRiskTypes.has(d.coreType ?? '')).length;
|
|
192
|
+
if (highRiskTypes.size > 0) {
|
|
193
|
+
score += Math.min(highRiskCount / Math.max(highRiskTypes.size, 3), 1) * 0.2;
|
|
194
|
+
}
|
|
195
|
+
// Factor 4: Transitive impact (max 0.2)
|
|
196
|
+
score += Math.min(Math.log10(transitiveDependents.length + 1) / 3, 0.2);
|
|
197
|
+
return Math.min(score, 1);
|
|
198
|
+
};
|
|
199
|
+
const getRiskLevel = (score) => {
|
|
200
|
+
if (score >= 0.75)
|
|
201
|
+
return 'CRITICAL';
|
|
202
|
+
if (score >= 0.5)
|
|
203
|
+
return 'HIGH';
|
|
204
|
+
if (score >= 0.25)
|
|
205
|
+
return 'MEDIUM';
|
|
206
|
+
return 'LOW';
|
|
207
|
+
};
|
|
208
|
+
const groupByType = (dependents) => {
|
|
209
|
+
const groups = {};
|
|
210
|
+
for (const dep of dependents) {
|
|
211
|
+
const type = dep.semanticType ?? dep.coreType ?? dep.labels?.[0] ?? 'Unknown';
|
|
212
|
+
groups[type] = (groups[type] ?? 0) + 1;
|
|
213
|
+
}
|
|
214
|
+
return groups;
|
|
215
|
+
};
|
|
216
|
+
const groupByRelationship = (dependents) => {
|
|
217
|
+
const groups = {};
|
|
218
|
+
for (const dep of dependents) {
|
|
219
|
+
groups[dep.relationshipType] = (groups[dep.relationshipType] ?? 0) + 1;
|
|
220
|
+
}
|
|
221
|
+
return groups;
|
|
222
|
+
};
|
|
223
|
+
const getAffectedFiles = (dependents) => {
|
|
224
|
+
const files = new Set();
|
|
225
|
+
for (const dep of dependents) {
|
|
226
|
+
if (dep.filePath)
|
|
227
|
+
files.add(dep.filePath);
|
|
228
|
+
}
|
|
229
|
+
return Array.from(files).sort();
|
|
230
|
+
};
|
|
231
|
+
const getMaxDepth = (dependents) => {
|
|
232
|
+
if (dependents.length === 0)
|
|
233
|
+
return 0;
|
|
234
|
+
return Math.max(...dependents.map((d) => d.depth ?? 1));
|
|
235
|
+
};
|
|
236
|
+
const findCriticalPaths = (directDependents, target, weights) => {
|
|
237
|
+
const paths = [];
|
|
238
|
+
for (const dep of directDependents) {
|
|
239
|
+
const relWeight = weights[dep.relationshipType] ?? 0.5;
|
|
240
|
+
// Only include high-weight relationships
|
|
241
|
+
if (relWeight >= 0.6) {
|
|
242
|
+
const depType = dep.semanticType ?? dep.coreType ?? '';
|
|
243
|
+
paths.push(`${dep.name} (${depType}) -[${dep.relationshipType}]-> ${target.name} (${target.type})`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return paths.slice(0, 10);
|
|
247
|
+
};
|
|
248
|
+
const buildSummary = (target, directCount, transitiveCount, fileCount, riskLevel) => {
|
|
249
|
+
if (directCount === 0) {
|
|
250
|
+
return `${target.name} (${target.type}) has no external dependents - safe to modify`;
|
|
251
|
+
}
|
|
252
|
+
return `Modifying ${target.name} (${target.type}) affects ${directCount} direct and ${transitiveCount} transitive dependents across ${fileCount} files. Risk: ${riskLevel}`;
|
|
253
|
+
};
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Centralized tool creation and registration
|
|
4
4
|
*/
|
|
5
5
|
import { createHelloTool } from './hello.tool.js';
|
|
6
|
+
import { createImpactAnalysisTool } from './impact-analysis.tool.js';
|
|
6
7
|
import { createNaturalLanguageToCypherTool } from './natural-language-to-cypher.tool.js';
|
|
7
8
|
import { createParseTypescriptProjectTool } from './parse-typescript-project.tool.js';
|
|
8
9
|
import { createSearchCodebaseTool } from './search-codebase.tool.js';
|
|
@@ -19,6 +20,7 @@ export const registerAllTools = (server) => {
|
|
|
19
20
|
createSearchCodebaseTool(server);
|
|
20
21
|
createTraverseFromNodeTool(server);
|
|
21
22
|
createNaturalLanguageToCypherTool(server);
|
|
23
|
+
createImpactAnalysisTool(server);
|
|
22
24
|
// Register project parsing tool
|
|
23
25
|
createParseTypescriptProjectTool(server);
|
|
24
26
|
};
|