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.
@@ -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
- * Enrich raw Neo4j schema with FairSquare domain context
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 enrichSchemaWithDomainContext = (rawSchema) => {
21
- return {
22
- rawSchema,
23
- domainContext: {
24
- framework: 'FairSquare',
25
- description: 'Custom TypeScript framework for microservices with dependency injection and repository patterns',
26
- nodeTypes: {
27
- Controller: {
28
- description: 'HTTP request handlers that extend the Controller base class',
29
- purpose: 'Entry points for HTTP API endpoints',
30
- commonProperties: ['name', 'filePath', 'sourceCode'],
31
- exampleQuery: 'MATCH (c:Controller) WHERE c.name =~ ".*Credit.*" RETURN c',
32
- },
33
- Service: {
34
- description: 'Business logic layer with @Injectable decorator',
35
- purpose: 'Encapsulate business logic and orchestrate data operations',
36
- commonProperties: ['name', 'filePath', 'dependencies'],
37
- exampleQuery: 'MATCH (s:Service)-[:INJECTS]->(dep) RETURN s.name, collect(dep.name) as dependencies',
38
- },
39
- Repository: {
40
- description: 'Data access layer that extends Repository base class',
41
- purpose: 'Abstract database operations and provide data access interface',
42
- commonProperties: ['name', 'filePath', 'dals'],
43
- exampleQuery: 'MATCH (r:Repository)-[:USES_DAL]->(d:DAL) RETURN r.name, collect(d.name) as dals',
44
- },
45
- DAL: {
46
- description: 'Data Access Layer - direct database interaction classes',
47
- purpose: 'Execute database queries and manage data persistence',
48
- commonProperties: ['name', 'filePath'],
49
- exampleQuery: 'MATCH (d:DAL)<-[:USES_DAL]-(r:Repository) RETURN d.name, count(r) as usedByCount',
50
- },
51
- PermissionManager: {
52
- description: 'Security layer for authorization checks',
53
- purpose: 'Control access to resources and validate permissions',
54
- commonProperties: ['name', 'filePath'],
55
- exampleQuery: 'MATCH (c:Controller)-[:PROTECTED_BY]->(pm:PermissionManager) RETURN c.name, pm.name',
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 caching it locally
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
- // Enrich schema with FairSquare domain context
149
- const enrichedSchema = enrichSchemaWithDomainContext(rawSchema);
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(enrichedSchema, null, LOG_CONFIG.jsonIndentation));
152
- await debugLog('Neo4j schema cached successfully with domain context', { schemaPath });
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
+ };
@@ -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
  };