code-graph-context 1.0.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/dist/core/embeddings/natural-language-to-cypher.service.js +12 -13
- package/dist/core/parsers/parser-factory.js +1 -3
- package/dist/core/parsers/typescript-parser.js +28 -2
- 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 +7 -2
- package/dist/mcp/tools/search-codebase.tool.js +4 -10
- package/dist/mcp/utils.js +2 -1
- package/dist/storage/neo4j/neo4j.service.js +140 -0
- package/package.json +1 -1
|
@@ -7,10 +7,11 @@ export class NaturalLanguageToCypherService {
|
|
|
7
7
|
messageInstructions = `
|
|
8
8
|
The schema file (neo4j-apoc-schema.json) contains two sections:
|
|
9
9
|
1. rawSchema: Complete Neo4j APOC schema with all node labels, properties, and relationships in the graph
|
|
10
|
-
2.
|
|
11
|
-
- nodeTypes:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
10
|
+
2. discoveredSchema: Dynamically discovered graph structure including:
|
|
11
|
+
- nodeTypes: Array of {label, count, properties} for each node type in the graph
|
|
12
|
+
- relationshipTypes: Array of {type, count, connections} showing relationship types and what they connect
|
|
13
|
+
- semanticTypes: Array of {type, count} showing semantic node classifications (e.g., Service, Controller)
|
|
14
|
+
- commonPatterns: Array of {from, relationship, to, count} showing frequent relationship patterns
|
|
14
15
|
|
|
15
16
|
Your response must be a valid JSON object with this exact structure:
|
|
16
17
|
{
|
|
@@ -20,19 +21,17 @@ Your response must be a valid JSON object with this exact structure:
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
Query Generation Process:
|
|
23
|
-
1. CHECK
|
|
24
|
-
2.
|
|
25
|
-
3. CHECK
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
24
|
+
1. CHECK NODE TYPES: Look at discoveredSchema.nodeTypes to see available node labels and their properties
|
|
25
|
+
2. CHECK RELATIONSHIPS: Look at discoveredSchema.relationshipTypes to understand how nodes connect
|
|
26
|
+
3. CHECK SEMANTIC TYPES: Look at discoveredSchema.semanticTypes for higher-level node classifications
|
|
27
|
+
4. REVIEW PATTERNS: Check discoveredSchema.commonPatterns for frequent relationship patterns in the graph
|
|
28
|
+
5. EXAMINE PROPERTIES: Use rawSchema for exact property names and types
|
|
28
29
|
6. GENERATE QUERY: Write the Cypher query using only node labels, relationships, and properties that exist in the schema
|
|
29
30
|
|
|
30
31
|
Critical Rules:
|
|
31
32
|
- Use the schema information from the file_search tool - do not guess node labels or relationships
|
|
32
|
-
- Use ONLY node labels and properties found in
|
|
33
|
-
- For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName)
|
|
34
|
-
- Check domainContext for parsing instructions specific to certain node types (e.g., some nodes may store arrays of objects in JSON format)
|
|
35
|
-
- Follow the example queries in commonQueryPatterns for proper syntax patterns
|
|
33
|
+
- Use ONLY node labels and properties found in the schema
|
|
34
|
+
- For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName)
|
|
36
35
|
- Use parameterized queries with $ syntax for any dynamic values
|
|
37
36
|
- Return only the data relevant to the user's request
|
|
38
37
|
|
|
@@ -69,9 +69,7 @@ export class ParserFactory {
|
|
|
69
69
|
...packageJson.devDependencies,
|
|
70
70
|
};
|
|
71
71
|
const hasNestJS = '@nestjs/common' in deps || '@nestjs/core' in deps;
|
|
72
|
-
const hasFairSquare = '@fairsquare/core' in deps ||
|
|
73
|
-
'@fairsquare/server' in deps ||
|
|
74
|
-
packageJson.name === '@fairsquare/source';
|
|
72
|
+
const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps || packageJson.name === '@fairsquare/source';
|
|
75
73
|
if (hasFairSquare && hasNestJS) {
|
|
76
74
|
return ProjectType.BOTH;
|
|
77
75
|
}
|
|
@@ -171,6 +171,14 @@ export class TypeScriptParser {
|
|
|
171
171
|
this.addNode(coreNode);
|
|
172
172
|
const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
|
|
173
173
|
this.addEdge(coreEdge);
|
|
174
|
+
const SKELETONIZE_TYPES = new Set([
|
|
175
|
+
CoreNodeType.METHOD_DECLARATION,
|
|
176
|
+
CoreNodeType.FUNCTION_DECLARATION,
|
|
177
|
+
CoreNodeType.PROPERTY_DECLARATION,
|
|
178
|
+
]);
|
|
179
|
+
if (SKELETONIZE_TYPES.has(type)) {
|
|
180
|
+
this.skeletonizeChildInParent(parentNode, coreNode);
|
|
181
|
+
}
|
|
174
182
|
const childNodeConfig = this.coreSchema.nodeTypes[type];
|
|
175
183
|
if (childNodeConfig) {
|
|
176
184
|
this.queueRelationshipNodes(childNodeConfig, coreNode, child);
|
|
@@ -179,6 +187,15 @@ export class TypeScriptParser {
|
|
|
179
187
|
}
|
|
180
188
|
}
|
|
181
189
|
}
|
|
190
|
+
skeletonizeChildInParent(parent, child) {
|
|
191
|
+
const childText = child.properties.sourceCode;
|
|
192
|
+
const bodyStart = childText.indexOf('{');
|
|
193
|
+
if (bodyStart > -1) {
|
|
194
|
+
const signature = childText.substring(0, bodyStart).trim();
|
|
195
|
+
const placeholder = `${signature} { /* NodeID: ${child.id} */ }`;
|
|
196
|
+
parent.properties.sourceCode = parent.properties.sourceCode.replace(childText, placeholder);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
182
199
|
/**
|
|
183
200
|
* Queue relationship edges for deferred processing
|
|
184
201
|
* These are resolved after all nodes are parsed since the target may not exist yet
|
|
@@ -218,8 +235,12 @@ export class TypeScriptParser {
|
|
|
218
235
|
return target.getName();
|
|
219
236
|
if (Node.isInterfaceDeclaration(target))
|
|
220
237
|
return target.getName();
|
|
221
|
-
if (Node.isExpressionWithTypeArguments(target))
|
|
222
|
-
|
|
238
|
+
if (Node.isExpressionWithTypeArguments(target)) {
|
|
239
|
+
const expression = target.getExpression();
|
|
240
|
+
const text = expression.getText();
|
|
241
|
+
const genericIndex = text.indexOf('<');
|
|
242
|
+
return genericIndex > 0 ? text.substring(0, genericIndex) : text;
|
|
243
|
+
}
|
|
223
244
|
return undefined;
|
|
224
245
|
}
|
|
225
246
|
/**
|
|
@@ -231,6 +252,11 @@ export class TypeScriptParser {
|
|
|
231
252
|
return node;
|
|
232
253
|
}
|
|
233
254
|
}
|
|
255
|
+
for (const node of this.existingNodes.values()) {
|
|
256
|
+
if (node.coreType === coreType && node.properties.name === name) {
|
|
257
|
+
return node;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
234
260
|
return undefined;
|
|
235
261
|
}
|
|
236
262
|
/**
|
package/dist/mcp/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ export const TOOL_NAMES = {
|
|
|
21
21
|
traverseFromNode: 'traverse_from_node',
|
|
22
22
|
parseTypescriptProject: 'parse_typescript_project',
|
|
23
23
|
testNeo4jConnection: 'test_neo4j_connection',
|
|
24
|
+
impactAnalysis: 'impact_analysis',
|
|
24
25
|
};
|
|
25
26
|
// Tool Metadata
|
|
26
27
|
export const TOOL_METADATA = {
|
|
@@ -39,7 +40,6 @@ Start with default parameters for richest context in a single call. Most queries
|
|
|
39
40
|
|
|
40
41
|
Parameters:
|
|
41
42
|
- query: Natural language description of what you're looking for
|
|
42
|
-
- limit (default: 10): Number of initial vector search results to consider
|
|
43
43
|
|
|
44
44
|
**Token Optimization (Only if needed)**:
|
|
45
45
|
Use these parameters ONLY if you encounter token limit errors (>25,000 tokens):
|
|
@@ -87,10 +87,27 @@ Best practices:
|
|
|
87
87
|
title: 'Test Neo4j Connection & APOC',
|
|
88
88
|
description: 'Test connection to Neo4j database and verify APOC plugin is available',
|
|
89
89
|
},
|
|
90
|
+
[TOOL_NAMES.impactAnalysis]: {
|
|
91
|
+
title: 'Impact Analysis',
|
|
92
|
+
description: `Analyze the impact of modifying a code node. Shows what depends on this node and helps assess risk before making changes.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
- Risk level (LOW/MEDIUM/HIGH/CRITICAL) based on dependency count and relationship types
|
|
96
|
+
- Direct dependents: nodes that directly reference the target
|
|
97
|
+
- Transitive dependents: nodes affected through dependency chains
|
|
98
|
+
- Affected files: list of files that would need review
|
|
99
|
+
- Critical paths: high-risk dependency chains
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
- nodeId: Node ID from search_codebase or traverse_from_node results
|
|
103
|
+
- filePath: Alternative - analyze all exports from a file
|
|
104
|
+
- maxDepth: How far to trace transitive dependencies (default: 4)
|
|
105
|
+
|
|
106
|
+
Use this before refactoring to understand blast radius of changes.`,
|
|
107
|
+
},
|
|
90
108
|
};
|
|
91
109
|
// Default Values
|
|
92
110
|
export const DEFAULTS = {
|
|
93
|
-
searchLimit: 10,
|
|
94
111
|
traversalDepth: 3,
|
|
95
112
|
skipOffset: 0,
|
|
96
113
|
batchSize: 500,
|
package/dist/mcp/mcp.server.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// Load environment variables from .env file
|
|
8
8
|
import dotenv from 'dotenv';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
9
|
import { dirname, join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = dirname(__filename);
|
|
13
13
|
// Go up two levels from dist/mcp/mcp.server.js to the root
|
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
|
};
|
|
@@ -63,7 +63,9 @@ export const createParseTypescriptProjectTool = (server) => {
|
|
|
63
63
|
// Recreate cross-file edges after incremental parse
|
|
64
64
|
if (!clearExisting && savedCrossFileEdges.length > 0) {
|
|
65
65
|
await debugLog('Recreating cross-file edges', { edgesToRecreate: savedCrossFileEdges.length });
|
|
66
|
-
const recreateResult = await neo4jService.run(QUERIES.RECREATE_CROSS_FILE_EDGES, {
|
|
66
|
+
const recreateResult = await neo4jService.run(QUERIES.RECREATE_CROSS_FILE_EDGES, {
|
|
67
|
+
edges: savedCrossFileEdges,
|
|
68
|
+
});
|
|
67
69
|
const recreatedCount = recreateResult[0]?.recreatedCount ?? 0;
|
|
68
70
|
await debugLog('Cross-file edges recreated', { recreatedCount, expected: savedCrossFileEdges.length });
|
|
69
71
|
}
|
|
@@ -112,7 +114,10 @@ const parseProject = async (options) => {
|
|
|
112
114
|
await deleteSourceFileSubgraphs(neo4jService, filesToRemoveFromGraph);
|
|
113
115
|
}
|
|
114
116
|
if (filesToReparse.length > 0) {
|
|
115
|
-
await debugLog('Incremental parse starting', {
|
|
117
|
+
await debugLog('Incremental parse starting', {
|
|
118
|
+
filesChanged: filesToReparse.length,
|
|
119
|
+
filesDeleted: filesToDelete.length,
|
|
120
|
+
});
|
|
116
121
|
// Load existing nodes from Neo4j for edge target matching
|
|
117
122
|
const existingNodes = await loadExistingNodesForEdgeDetection(neo4jService, filesToRemoveFromGraph);
|
|
118
123
|
await debugLog('Loaded existing nodes for edge detection', { count: existingNodes.length });
|
|
@@ -14,12 +14,6 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
14
14
|
description: TOOL_METADATA[TOOL_NAMES.searchCodebase].description,
|
|
15
15
|
inputSchema: {
|
|
16
16
|
query: z.string().describe('Natural language query to search the codebase'),
|
|
17
|
-
limit: z
|
|
18
|
-
.number()
|
|
19
|
-
.int()
|
|
20
|
-
.optional()
|
|
21
|
-
.describe(`Maximum number of results to return (default: ${DEFAULTS.searchLimit})`)
|
|
22
|
-
.default(DEFAULTS.searchLimit),
|
|
23
17
|
maxDepth: z
|
|
24
18
|
.number()
|
|
25
19
|
.int()
|
|
@@ -50,19 +44,19 @@ export const createSearchCodebaseTool = (server) => {
|
|
|
50
44
|
.describe('Use weighted traversal strategy that scores each node for relevance (default: false)')
|
|
51
45
|
.default(true),
|
|
52
46
|
},
|
|
53
|
-
}, async ({ query,
|
|
47
|
+
}, async ({ query, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, useWeightedTraversal = true, }) => {
|
|
54
48
|
try {
|
|
55
|
-
await debugLog('Search codebase started', { query
|
|
49
|
+
await debugLog('Search codebase started', { query });
|
|
56
50
|
const neo4jService = new Neo4jService();
|
|
57
51
|
const embeddingsService = new EmbeddingsService();
|
|
58
52
|
const traversalHandler = new TraversalHandler(neo4jService);
|
|
59
53
|
const embedding = await embeddingsService.embedText(query);
|
|
60
54
|
const vectorResults = await neo4jService.run(QUERIES.VECTOR_SEARCH, {
|
|
61
|
-
limit:
|
|
55
|
+
limit: 1,
|
|
62
56
|
embedding,
|
|
63
57
|
});
|
|
64
58
|
if (vectorResults.length === 0) {
|
|
65
|
-
await debugLog('No relevant code found', { query
|
|
59
|
+
await debugLog('No relevant code found', { query });
|
|
66
60
|
return createSuccessResponse(MESSAGES.errors.noRelevantCode);
|
|
67
61
|
}
|
|
68
62
|
const startNode = vectorResults[0].node;
|
package/dist/mcp/utils.js
CHANGED
|
@@ -55,7 +55,8 @@ export const formatNodeInfo = (value, key) => {
|
|
|
55
55
|
else {
|
|
56
56
|
// Show first 500 and last 500 characters
|
|
57
57
|
const half = Math.floor(maxLength / 2);
|
|
58
|
-
result.sourceCode =
|
|
58
|
+
result.sourceCode =
|
|
59
|
+
code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
|
|
59
60
|
result.hasMore = true;
|
|
60
61
|
result.truncated = code.length - maxLength;
|
|
61
62
|
}
|
|
@@ -329,4 +329,144 @@ export const QUERIES = {
|
|
|
329
329
|
LIMIT ${maxNodesPerDepth}
|
|
330
330
|
`;
|
|
331
331
|
},
|
|
332
|
+
// ============================================
|
|
333
|
+
// DYNAMIC SCHEMA DISCOVERY QUERIES
|
|
334
|
+
// ============================================
|
|
335
|
+
/**
|
|
336
|
+
* Get all distinct node labels with counts and sample properties
|
|
337
|
+
*/
|
|
338
|
+
DISCOVER_NODE_TYPES: `
|
|
339
|
+
CALL db.labels() YIELD label
|
|
340
|
+
CALL {
|
|
341
|
+
WITH label
|
|
342
|
+
MATCH (n) WHERE label IN labels(n)
|
|
343
|
+
WITH n LIMIT 1
|
|
344
|
+
RETURN keys(n) AS sampleProperties
|
|
345
|
+
}
|
|
346
|
+
CALL {
|
|
347
|
+
WITH label
|
|
348
|
+
MATCH (n) WHERE label IN labels(n)
|
|
349
|
+
RETURN count(n) AS nodeCount
|
|
350
|
+
}
|
|
351
|
+
RETURN label, nodeCount, sampleProperties
|
|
352
|
+
ORDER BY nodeCount DESC
|
|
353
|
+
`,
|
|
354
|
+
/**
|
|
355
|
+
* Get all distinct relationship types with counts and which node types they connect
|
|
356
|
+
*/
|
|
357
|
+
DISCOVER_RELATIONSHIP_TYPES: `
|
|
358
|
+
CALL db.relationshipTypes() YIELD relationshipType
|
|
359
|
+
CALL {
|
|
360
|
+
WITH relationshipType
|
|
361
|
+
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType
|
|
362
|
+
WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
|
|
363
|
+
RETURN fromLabel, toLabel
|
|
364
|
+
LIMIT 10
|
|
365
|
+
}
|
|
366
|
+
CALL {
|
|
367
|
+
WITH relationshipType
|
|
368
|
+
MATCH ()-[r]->() WHERE type(r) = relationshipType
|
|
369
|
+
RETURN count(r) AS relCount
|
|
370
|
+
}
|
|
371
|
+
RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
|
|
372
|
+
ORDER BY relCount DESC
|
|
373
|
+
`,
|
|
374
|
+
/**
|
|
375
|
+
* Get sample nodes of each semantic type for context
|
|
376
|
+
*/
|
|
377
|
+
DISCOVER_SEMANTIC_TYPES: `
|
|
378
|
+
MATCH (n)
|
|
379
|
+
WHERE n.semanticType IS NOT NULL
|
|
380
|
+
WITH n.semanticType AS semanticType, count(*) AS count
|
|
381
|
+
ORDER BY count DESC
|
|
382
|
+
RETURN semanticType, count
|
|
383
|
+
`,
|
|
384
|
+
/**
|
|
385
|
+
* Get example query patterns based on actual graph structure
|
|
386
|
+
*/
|
|
387
|
+
DISCOVER_COMMON_PATTERNS: `
|
|
388
|
+
MATCH (a)-[r]->(b)
|
|
389
|
+
WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
|
|
390
|
+
WHERE count > 5
|
|
391
|
+
RETURN fromType, relType, toType, count
|
|
392
|
+
ORDER BY count DESC
|
|
393
|
+
LIMIT 20
|
|
394
|
+
`,
|
|
395
|
+
// ============================================
|
|
396
|
+
// IMPACT ANALYSIS QUERIES
|
|
397
|
+
// Reuses cross-file edge pattern to find dependents
|
|
398
|
+
// ============================================
|
|
399
|
+
/**
|
|
400
|
+
* Get node details by ID
|
|
401
|
+
*/
|
|
402
|
+
GET_NODE_BY_ID: `
|
|
403
|
+
MATCH (n) WHERE n.id = $nodeId
|
|
404
|
+
RETURN n.id AS id,
|
|
405
|
+
n.name AS name,
|
|
406
|
+
labels(n) AS labels,
|
|
407
|
+
n.semanticType AS semanticType,
|
|
408
|
+
n.coreType AS coreType,
|
|
409
|
+
n.filePath AS filePath
|
|
410
|
+
`,
|
|
411
|
+
/**
|
|
412
|
+
* Get impact of changing a node - finds all external nodes that depend on it
|
|
413
|
+
* Based on GET_CROSS_FILE_EDGES pattern but for a single node
|
|
414
|
+
*/
|
|
415
|
+
GET_NODE_IMPACT: `
|
|
416
|
+
MATCH (target) WHERE target.id = $nodeId
|
|
417
|
+
MATCH (dependent)-[r]->(target)
|
|
418
|
+
WHERE dependent.id <> target.id
|
|
419
|
+
RETURN DISTINCT
|
|
420
|
+
dependent.id AS nodeId,
|
|
421
|
+
dependent.name AS name,
|
|
422
|
+
labels(dependent) AS labels,
|
|
423
|
+
dependent.semanticType AS semanticType,
|
|
424
|
+
dependent.coreType AS coreType,
|
|
425
|
+
dependent.filePath AS filePath,
|
|
426
|
+
type(r) AS relationshipType,
|
|
427
|
+
coalesce(r.relationshipWeight, 0.5) AS weight
|
|
428
|
+
`,
|
|
429
|
+
/**
|
|
430
|
+
* Get impact of changing a file - finds all external nodes that depend on nodes in this file
|
|
431
|
+
* Directly reuses GET_CROSS_FILE_EDGES pattern
|
|
432
|
+
*/
|
|
433
|
+
GET_FILE_IMPACT: `
|
|
434
|
+
MATCH (sf:SourceFile {filePath: $filePath})
|
|
435
|
+
OPTIONAL MATCH (sf)-[*]->(child)
|
|
436
|
+
WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesInFile
|
|
437
|
+
UNWIND nodesInFile AS n
|
|
438
|
+
MATCH (dependent)-[r]->(n)
|
|
439
|
+
WHERE NOT dependent IN nodesInFile
|
|
440
|
+
RETURN DISTINCT
|
|
441
|
+
dependent.id AS nodeId,
|
|
442
|
+
dependent.name AS name,
|
|
443
|
+
labels(dependent) AS labels,
|
|
444
|
+
dependent.semanticType AS semanticType,
|
|
445
|
+
dependent.coreType AS coreType,
|
|
446
|
+
dependent.filePath AS filePath,
|
|
447
|
+
type(r) AS relationshipType,
|
|
448
|
+
coalesce(r.relationshipWeight, 0.5) AS weight,
|
|
449
|
+
n.id AS targetNodeId,
|
|
450
|
+
n.name AS targetNodeName
|
|
451
|
+
`,
|
|
452
|
+
/**
|
|
453
|
+
* Get transitive dependents - nodes that depend on dependents (for deeper impact)
|
|
454
|
+
*/
|
|
455
|
+
GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
|
|
456
|
+
MATCH (target) WHERE target.id = $nodeId
|
|
457
|
+
MATCH path = (dependent)-[*2..${maxDepth}]->(target)
|
|
458
|
+
WITH dependent,
|
|
459
|
+
length(path) AS depth,
|
|
460
|
+
[r IN relationships(path) | type(r)] AS relationshipPath
|
|
461
|
+
RETURN DISTINCT
|
|
462
|
+
dependent.id AS nodeId,
|
|
463
|
+
dependent.name AS name,
|
|
464
|
+
labels(dependent) AS labels,
|
|
465
|
+
dependent.semanticType AS semanticType,
|
|
466
|
+
dependent.coreType AS coreType,
|
|
467
|
+
dependent.filePath AS filePath,
|
|
468
|
+
depth,
|
|
469
|
+
relationshipPath
|
|
470
|
+
ORDER BY depth ASC
|
|
471
|
+
`,
|
|
332
472
|
};
|
package/package.json
CHANGED