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 CHANGED
@@ -1,5 +1,6 @@
1
1
  # Code Graph Context MCP Server
2
2
 
3
+ [![npm version](https://badge.fury.io/js/code-graph-context.svg)](https://www.npmjs.com/package/code-graph-context)
3
4
  [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
4
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.8-007ACC?logo=typescript&logoColor=white)](https://typescriptlang.org/)
5
6
  [![Neo4j](https://img.shields.io/badge/Neo4j-5.0+-018bff?logo=neo4j&logoColor=white)](https://neo4j.com/)
@@ -147,6 +148,84 @@ Then configure in your MCP config file (`~/.config/claude/config.json`):
147
148
 
148
149
  **Note:** The env vars can be configured for any Neo4j instance - local, Docker, cloud (Aura), or enterprise.
149
150
 
151
+ ### Verify Installation
152
+
153
+ After installation, verify everything is working:
154
+
155
+ 1. **Check Neo4j is running:**
156
+ ```bash
157
+ # Open Neo4j Browser
158
+ open http://localhost:7474
159
+ # Login: neo4j / PASSWORD
160
+ ```
161
+
162
+ 2. **Test APOC plugin:**
163
+ ```cypher
164
+ CALL apoc.help("apoc")
165
+ ```
166
+ Should return a list of APOC functions.
167
+
168
+ 3. **Test MCP server connection:**
169
+ ```bash
170
+ claude mcp list
171
+ ```
172
+ Should show: `code-graph-context: ✓ Connected`
173
+
174
+ ### Troubleshooting
175
+
176
+ **"APOC plugin not found"**
177
+ ```bash
178
+ # Check Neo4j logs
179
+ docker logs code-graph-neo4j
180
+
181
+ # Verify APOC loaded
182
+ docker exec code-graph-neo4j cypher-shell -u neo4j -p PASSWORD "CALL apoc.help('apoc')"
183
+
184
+ # Restart if needed
185
+ docker restart code-graph-neo4j
186
+ ```
187
+
188
+ **"OPENAI_API_KEY environment variable is required"**
189
+ - Get your API key from: https://platform.openai.com/api-keys
190
+ - Add to Claude Code MCP config `env` section
191
+ - Verify with: `echo $OPENAI_API_KEY` (if using shell env)
192
+
193
+ **"Connection refused bolt://localhost:7687"**
194
+ ```bash
195
+ # Check Neo4j is running
196
+ docker ps | grep neo4j
197
+
198
+ # Check ports are not in use
199
+ lsof -i :7687
200
+
201
+ # Start Neo4j if stopped
202
+ docker start code-graph-neo4j
203
+
204
+ # Check Neo4j logs
205
+ docker logs code-graph-neo4j
206
+ ```
207
+
208
+ **"Neo4j memory errors"**
209
+ ```bash
210
+ # Increase memory in docker-compose.yml or docker run:
211
+ -e NEO4J_server_memory_heap_max__size=8G
212
+ -e NEO4J_dbms_memory_transaction_total_max=8G
213
+
214
+ docker restart code-graph-neo4j
215
+ ```
216
+
217
+ **"MCP server not responding"**
218
+ ```bash
219
+ # Check Claude Code logs
220
+ cat ~/Library/Logs/Claude/mcp*.log
221
+
222
+ # Test server directly
223
+ node /path/to/code-graph-context/dist/mcp/mcp.server.js
224
+
225
+ # Rebuild if needed
226
+ npm run build
227
+ ```
228
+
150
229
  ## Tool Usage Guide & Sequential Workflows
151
230
 
152
231
  ### Sequential Tool Usage Patterns
package/dist/constants.js CHANGED
@@ -1 +1,22 @@
1
1
  export const MAX_TRAVERSAL_DEPTH = 5;
2
+ // Shared exclude patterns for file parsing and change detection
3
+ // Regex patterns (escaped dots, anchored to end)
4
+ export const EXCLUDE_PATTERNS_REGEX = [
5
+ 'node_modules/',
6
+ 'dist/',
7
+ 'build/',
8
+ 'coverage/',
9
+ '\\.d\\.ts$',
10
+ '\\.spec\\.ts$',
11
+ '\\.test\\.ts$',
12
+ ];
13
+ // Glob patterns for use with glob library
14
+ export const EXCLUDE_PATTERNS_GLOB = [
15
+ 'node_modules/**',
16
+ 'dist/**',
17
+ 'build/**',
18
+ 'coverage/**',
19
+ '**/*.d.ts',
20
+ '**/*.spec.ts',
21
+ '**/*.test.ts',
22
+ ];
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
3
4
  import { CoreNodeType, SemanticNodeType, SemanticEdgeType, } from './schema.js';
4
5
  // ============================================================================
5
6
  // NESTJS HELPER FUNCTIONS
@@ -888,7 +889,7 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
888
889
  // ============================================================================
889
890
  export const NESTJS_PARSE_OPTIONS = {
890
891
  includePatterns: ['**/*.ts', '**/*.tsx'],
891
- excludePatterns: ['node_modules/', 'dist/', 'coverage/', '.d.ts', '.spec.ts', '.test.ts'],
892
+ excludePatterns: EXCLUDE_PATTERNS_REGEX,
892
893
  maxFiles: 1000,
893
894
  frameworkSchemas: [NESTJS_FRAMEWORK_SCHEMA],
894
895
  };
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  // graph.ts - Optimized for Neo4j performance with context-based framework properties
3
+ import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
3
4
  // ============================================================================
4
5
  // CORE ENUMS
5
6
  // ============================================================================
@@ -792,7 +793,7 @@ export const CORE_TYPESCRIPT_SCHEMA = {
792
793
  };
793
794
  export const DEFAULT_PARSE_OPTIONS = {
794
795
  includePatterns: ['**/*.ts', '**/*.tsx'],
795
- excludePatterns: ['node_modules/', 'dist/', 'coverage/', '.d.ts', '.spec.ts', '.test.ts'],
796
+ excludePatterns: EXCLUDE_PATTERNS_REGEX,
796
797
  maxFiles: 1000,
797
798
  coreSchema: CORE_TYPESCRIPT_SCHEMA,
798
799
  frameworkSchemas: [],
@@ -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. domainContext: Framework-specific semantics including:
11
- - nodeTypes: Descriptions and example queries for each node type
12
- - relationships: How nodes connect with context about relationship properties
13
- - commonQueryPatterns: Pre-built example queries for common use cases
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 DOMAIN CONTEXT: Look at domainContext.nodeTypes to understand available node types and their properties
24
- 2. REVIEW EXAMPLES: Check domainContext.commonQueryPatterns for similar query examples
25
- 3. CHECK RELATIONSHIPS: Look at domainContext.relationships to understand how nodes connect
26
- 4. EXAMINE NODE PROPERTIES: Use rawSchema to see exact property names and types
27
- 5. HANDLE JSON PROPERTIES: If properties or relationship context are stored as JSON strings, use apoc.convert.fromJsonMap() to parse them
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 rawSchema
33
- - For nested JSON data in properties, use: apoc.convert.fromJsonMap(node.propertyName) or apoc.convert.fromJsonMap(relationship.context)
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
 
@@ -2,6 +2,7 @@
2
2
  * Parser Factory
3
3
  * Creates TypeScript parsers with appropriate framework schemas
4
4
  */
5
+ import { EXCLUDE_PATTERNS_REGEX } from '../../constants.js';
5
6
  import { FAIRSQUARE_FRAMEWORK_SCHEMA } from '../config/fairsquare-framework-schema.js';
6
7
  import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
7
8
  import { CORE_TYPESCRIPT_SCHEMA, CoreNodeType } from '../config/schema.js';
@@ -19,7 +20,7 @@ export class ParserFactory {
19
20
  */
20
21
  static createParser(options) {
21
22
  const { workspacePath, tsConfigPath = 'tsconfig.json', projectType = ProjectType.NESTJS, // Default to NestJS (use auto-detect for best results)
22
- customFrameworkSchemas = [], excludePatterns = ['node_modules', 'dist', 'build', '.spec.', '.test.'], excludedNodeTypes = [CoreNodeType.PARAMETER_DECLARATION], } = options;
23
+ customFrameworkSchemas = [], excludePatterns = EXCLUDE_PATTERNS_REGEX, excludedNodeTypes = [CoreNodeType.PARAMETER_DECLARATION], } = options;
23
24
  // Select framework schemas based on project type
24
25
  const frameworkSchemas = this.selectFrameworkSchemas(projectType, customFrameworkSchemas);
25
26
  console.log(`📦 Creating parser for ${projectType} project`);
@@ -68,7 +69,7 @@ export class ParserFactory {
68
69
  ...packageJson.devDependencies,
69
70
  };
70
71
  const hasNestJS = '@nestjs/common' in deps || '@nestjs/core' in deps;
71
- const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps;
72
+ const hasFairSquare = '@fairsquare/core' in deps || '@fairsquare/server' in deps || packageJson.name === '@fairsquare/source';
72
73
  if (hasFairSquare && hasNestJS) {
73
74
  return ProjectType.BOTH;
74
75
  }
@@ -1,8 +1,24 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import crypto from 'crypto';
3
+ import fs from 'fs/promises';
2
4
  import path from 'node:path';
3
5
  import { minimatch } from 'minimatch';
4
6
  import { Project, Node } from 'ts-morph';
5
- import { v4 as uuidv4 } from 'uuid';
7
+ /**
8
+ * Generate a deterministic node ID based on stable properties.
9
+ * This ensures the same node gets the same ID across reparses.
10
+ *
11
+ * Identity is based on: coreType + filePath + name (+ parentId for nested nodes)
12
+ * This is stable because when it matters (one side of edge not reparsed),
13
+ * names are guaranteed unchanged (or imports would break, triggering reparse).
14
+ */
15
+ const generateDeterministicId = (coreType, filePath, name, parentId) => {
16
+ const parts = parentId ? [coreType, filePath, parentId, name] : [coreType, filePath, name];
17
+ const identity = parts.join('::');
18
+ const hash = crypto.createHash('sha256').update(identity).digest('hex').substring(0, 16);
19
+ return `${coreType}:${hash}`;
20
+ };
21
+ import { hashFile } from '../../utils/file-utils.js';
6
22
  import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
7
23
  import { CoreNodeType, CORE_TYPESCRIPT_SCHEMA, DEFAULT_PARSE_OPTIONS, CoreEdgeType, } from '../config/schema.js';
8
24
  export class TypeScriptParser {
@@ -14,6 +30,7 @@ export class TypeScriptParser {
14
30
  frameworkSchemas;
15
31
  parsedNodes = new Map();
16
32
  parsedEdges = new Map();
33
+ existingNodes = new Map(); // Nodes from Neo4j for edge target matching
17
34
  deferredEdges = [];
18
35
  sharedContext = new Map(); // Shared context for custom data
19
36
  constructor(workspacePath, tsConfigPath = 'tsconfig.json', coreSchema = CORE_TYPESCRIPT_SCHEMA, frameworkSchemas = [NESTJS_FRAMEWORK_SCHEMA], parseConfig = DEFAULT_PARSE_OPTIONS) {
@@ -22,7 +39,6 @@ export class TypeScriptParser {
22
39
  this.coreSchema = coreSchema;
23
40
  this.frameworkSchemas = frameworkSchemas;
24
41
  this.parseConfig = parseConfig;
25
- // Initialize with proper compiler options for NestJS
26
42
  this.project = new Project({
27
43
  tsConfigFilePath: tsConfigPath,
28
44
  skipAddingFilesFromTsConfig: false,
@@ -36,25 +52,53 @@ export class TypeScriptParser {
36
52
  });
37
53
  this.project.addSourceFilesAtPaths(path.join(workspacePath, '**/*.ts'));
38
54
  }
39
- async parseWorkspace() {
40
- const sourceFiles = this.project.getSourceFiles();
41
- // Phase 1: Core parsing for ALL files
55
+ /**
56
+ * Set existing nodes from Neo4j for edge target matching during incremental parsing.
57
+ * These nodes will be available as targets for edge detection but won't be exported.
58
+ */
59
+ setExistingNodes(nodes) {
60
+ this.existingNodes.clear();
61
+ for (const node of nodes) {
62
+ // Convert to ParsedNode format (without AST)
63
+ const parsedNode = {
64
+ id: node.id,
65
+ coreType: node.coreType,
66
+ semanticType: node.semanticType,
67
+ labels: node.labels,
68
+ properties: {
69
+ id: node.id,
70
+ name: node.name,
71
+ coreType: node.coreType,
72
+ filePath: node.filePath,
73
+ semanticType: node.semanticType,
74
+ },
75
+ // No sourceNode - these are from Neo4j, not parsed
76
+ };
77
+ this.existingNodes.set(node.id, parsedNode);
78
+ }
79
+ console.log(`📦 Loaded ${nodes.length} existing nodes for edge detection`);
80
+ }
81
+ async parseWorkspace(filesToParse) {
82
+ let sourceFiles;
83
+ if (filesToParse && filesToParse.length > 0) {
84
+ sourceFiles = filesToParse
85
+ .map((filePath) => this.project.getSourceFile(filePath))
86
+ .filter((sf) => sf !== undefined);
87
+ }
88
+ else {
89
+ sourceFiles = this.project.getSourceFiles();
90
+ }
42
91
  for (const sourceFile of sourceFiles) {
43
92
  if (this.shouldSkipFile(sourceFile))
44
93
  continue;
45
94
  await this.parseCoreTypeScriptV2(sourceFile);
46
95
  }
47
- // Phase 1.5: Resolve deferred relationship edges (EXTENDS, IMPLEMENTS)
48
96
  this.resolveDeferredEdges();
49
- // Phase 2: Apply context extractors
50
97
  await this.applyContextExtractors();
51
- // Phase 3: Framework enhancements
52
98
  if (this.frameworkSchemas.length > 0) {
53
99
  await this.applyFrameworkEnhancements();
54
100
  }
55
- // Phase 4: Edge enhancements
56
101
  await this.applyEdgeEnhancements();
57
- // Convert to Neo4j format
58
102
  const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
59
103
  const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
60
104
  return { nodes: neo4jNodes, edges: neo4jEdges };
@@ -77,17 +121,22 @@ export class TypeScriptParser {
77
121
  return false;
78
122
  }
79
123
  async parseCoreTypeScriptV2(sourceFile) {
80
- const sourceFileNode = this.createCoreNode(sourceFile, CoreNodeType.SOURCE_FILE);
124
+ const filePath = sourceFile.getFilePath();
125
+ const stats = await fs.stat(filePath);
126
+ const fileTrackingProperties = {
127
+ size: stats.size,
128
+ mtime: stats.mtimeMs,
129
+ contentHash: await hashFile(filePath),
130
+ };
131
+ const sourceFileNode = this.createCoreNode(sourceFile, CoreNodeType.SOURCE_FILE, fileTrackingProperties);
81
132
  this.addNode(sourceFileNode);
82
- // Parse configured children
83
- this.parseChildNodes(this.coreSchema.nodeTypes[CoreNodeType.SOURCE_FILE], sourceFileNode, sourceFile);
84
- // Special handling: Parse variable declarations if framework schema specifies patterns
133
+ await this.parseChildNodes(this.coreSchema.nodeTypes[CoreNodeType.SOURCE_FILE], sourceFileNode, sourceFile);
85
134
  if (this.shouldParseVariables(sourceFile.getFilePath())) {
86
135
  for (const varStatement of sourceFile.getVariableStatements()) {
87
136
  for (const varDecl of varStatement.getDeclarations()) {
88
137
  if (this.shouldSkipChildNode(varDecl))
89
138
  continue;
90
- const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION);
139
+ const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
91
140
  this.addNode(variableNode);
92
141
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
93
142
  this.addEdge(containsEdge);
@@ -118,10 +167,18 @@ export class TypeScriptParser {
118
167
  for (const child of children) {
119
168
  if (this.shouldSkipChildNode(child))
120
169
  continue;
121
- const coreNode = this.createCoreNode(child, type);
170
+ const coreNode = this.createCoreNode(child, type, {}, parentNode.id);
122
171
  this.addNode(coreNode);
123
172
  const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
124
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
+ }
125
182
  const childNodeConfig = this.coreSchema.nodeTypes[type];
126
183
  if (childNodeConfig) {
127
184
  this.queueRelationshipNodes(childNodeConfig, coreNode, child);
@@ -130,6 +187,15 @@ export class TypeScriptParser {
130
187
  }
131
188
  }
132
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
+ }
133
199
  /**
134
200
  * Queue relationship edges for deferred processing
135
201
  * These are resolved after all nodes are parsed since the target may not exist yet
@@ -169,8 +235,12 @@ export class TypeScriptParser {
169
235
  return target.getName();
170
236
  if (Node.isInterfaceDeclaration(target))
171
237
  return target.getName();
172
- if (Node.isExpressionWithTypeArguments(target))
173
- return target.getExpression().getText();
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
+ }
174
244
  return undefined;
175
245
  }
176
246
  /**
@@ -182,6 +252,11 @@ export class TypeScriptParser {
182
252
  return node;
183
253
  }
184
254
  }
255
+ for (const node of this.existingNodes.values()) {
256
+ if (node.coreType === coreType && node.properties.name === name) {
257
+ return node;
258
+ }
259
+ }
185
260
  return undefined;
186
261
  }
187
262
  /**
@@ -205,14 +280,14 @@ export class TypeScriptParser {
205
280
  this.addNode(sourceFileNode);
206
281
  // Parse classes
207
282
  for (const classDecl of sourceFile.getClasses()) {
208
- const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION);
283
+ const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION, {}, sourceFileNode.id);
209
284
  this.addNode(classNode);
210
285
  // File contains class relationship
211
286
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, classNode.id);
212
287
  this.addEdge(containsEdge);
213
288
  // Parse class decorators
214
289
  for (const decorator of classDecl.getDecorators()) {
215
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
290
+ const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, classNode.id);
216
291
  this.addNode(decoratorNode);
217
292
  // Class decorated with decorator relationship
218
293
  const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, classNode.id, decoratorNode.id);
@@ -220,14 +295,14 @@ export class TypeScriptParser {
220
295
  }
221
296
  // Parse methods
222
297
  for (const method of classDecl.getMethods()) {
223
- const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION);
298
+ const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION, {}, classNode.id);
224
299
  this.addNode(methodNode);
225
300
  // Class has method relationship
226
301
  const hasMethodEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, methodNode.id);
227
302
  this.addEdge(hasMethodEdge);
228
303
  // Parse method decorators
229
304
  for (const decorator of method.getDecorators()) {
230
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
305
+ const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, methodNode.id);
231
306
  this.addNode(decoratorNode);
232
307
  // Method decorated with decorator relationship
233
308
  const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, methodNode.id, decoratorNode.id);
@@ -235,14 +310,14 @@ export class TypeScriptParser {
235
310
  }
236
311
  // Parse method parameters
237
312
  for (const param of method.getParameters()) {
238
- const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION);
313
+ const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, methodNode.id);
239
314
  this.addNode(paramNode);
240
315
  // Method has parameter relationship
241
316
  const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, methodNode.id, paramNode.id);
242
317
  this.addEdge(hasParamEdge);
243
318
  // Parse parameter decorators
244
319
  for (const decorator of param.getDecorators()) {
245
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
320
+ const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, paramNode.id);
246
321
  this.addNode(decoratorNode);
247
322
  // Parameter decorated with decorator relationship
248
323
  const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, paramNode.id, decoratorNode.id);
@@ -252,14 +327,14 @@ export class TypeScriptParser {
252
327
  }
253
328
  // Parse properties
254
329
  for (const property of classDecl.getProperties()) {
255
- const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION);
330
+ const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION, {}, classNode.id);
256
331
  this.addNode(propertyNode);
257
332
  // Class has property relationship
258
333
  const hasPropertyEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, propertyNode.id);
259
334
  this.addEdge(hasPropertyEdge);
260
335
  // Parse property decorators
261
336
  for (const decorator of property.getDecorators()) {
262
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR);
337
+ const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, propertyNode.id);
263
338
  this.addNode(decoratorNode);
264
339
  // Property decorated with decorator relationship
265
340
  const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, propertyNode.id, decoratorNode.id);
@@ -269,7 +344,7 @@ export class TypeScriptParser {
269
344
  }
270
345
  // Parse interfaces
271
346
  for (const interfaceDecl of sourceFile.getInterfaces()) {
272
- const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION);
347
+ const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION, {}, sourceFileNode.id);
273
348
  this.addNode(interfaceNode);
274
349
  // File contains interface relationship
275
350
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, interfaceNode.id);
@@ -277,14 +352,14 @@ export class TypeScriptParser {
277
352
  }
278
353
  // Parse functions
279
354
  for (const funcDecl of sourceFile.getFunctions()) {
280
- const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION);
355
+ const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION, {}, sourceFileNode.id);
281
356
  this.addNode(functionNode);
282
357
  // File contains function relationship
283
358
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, functionNode.id);
284
359
  this.addEdge(containsEdge);
285
360
  // Parse function parameters
286
361
  for (const param of funcDecl.getParameters()) {
287
- const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION);
362
+ const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, functionNode.id);
288
363
  this.addNode(paramNode);
289
364
  // Function has parameter relationship
290
365
  const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, functionNode.id, paramNode.id);
@@ -293,7 +368,7 @@ export class TypeScriptParser {
293
368
  }
294
369
  // Parse imports
295
370
  for (const importDecl of sourceFile.getImportDeclarations()) {
296
- const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION);
371
+ const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION, {}, sourceFileNode.id);
297
372
  this.addNode(importNode);
298
373
  // File contains import relationship
299
374
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, importNode.id);
@@ -303,7 +378,7 @@ export class TypeScriptParser {
303
378
  if (this.shouldParseVariables(sourceFile.getFilePath())) {
304
379
  for (const varStatement of sourceFile.getVariableStatements()) {
305
380
  for (const varDecl of varStatement.getDeclarations()) {
306
- const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION);
381
+ const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
307
382
  this.addNode(variableNode);
308
383
  // File contains variable relationship
309
384
  const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
@@ -316,18 +391,21 @@ export class TypeScriptParser {
316
391
  console.error(`Error parsing file ${sourceFile.getFilePath()}:`, error);
317
392
  }
318
393
  }
319
- createCoreNode(astNode, coreType) {
320
- const nodeId = `${coreType}:${uuidv4()}`;
394
+ createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
395
+ const name = this.extractNodeName(astNode, coreType);
396
+ const filePath = astNode.getSourceFile().getFilePath();
397
+ const nodeId = generateDeterministicId(coreType, filePath, name, parentId);
321
398
  // Extract base properties using schema
322
399
  const properties = {
323
400
  id: nodeId,
324
- name: this.extractNodeName(astNode, coreType),
401
+ name,
325
402
  coreType,
326
- filePath: astNode.getSourceFile().getFilePath(),
403
+ filePath,
327
404
  startLine: astNode.getStartLineNumber(),
328
405
  endLine: astNode.getEndLineNumber(),
329
406
  sourceCode: astNode.getText(),
330
407
  createdAt: new Date().toISOString(),
408
+ ...baseProperties,
331
409
  };
332
410
  // Extract schema-defined properties
333
411
  const coreNodeDef = this.coreSchema.nodeTypes[coreType];
@@ -386,8 +464,12 @@ export class TypeScriptParser {
386
464
  // Get the weight from the core schema
387
465
  const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[relationshipType];
388
466
  const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
467
+ // Generate deterministic edge ID based on type + source + target
468
+ const edgeIdentity = `${relationshipType}::${sourceNodeId}::${targetNodeId}`;
469
+ const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
470
+ const edgeId = `${relationshipType}:${edgeHash}`;
389
471
  return {
390
- id: `${relationshipType}:${uuidv4()}`,
472
+ id: edgeId,
391
473
  relationshipType,
392
474
  sourceNodeId,
393
475
  targetNodeId,
@@ -517,8 +599,14 @@ export class TypeScriptParser {
517
599
  }
518
600
  async applyEdgeEnhancement(edgeEnhancement) {
519
601
  try {
602
+ // Combine parsed nodes and existing nodes for target matching
603
+ // Sources must be parsed (have AST), targets can be either
604
+ const allTargetNodes = new Map([...this.parsedNodes, ...this.existingNodes]);
520
605
  for (const [sourceId, sourceNode] of this.parsedNodes) {
521
- for (const [targetId, targetNode] of this.parsedNodes) {
606
+ // Skip if source doesn't have AST (shouldn't happen for parsedNodes, but be safe)
607
+ if (!sourceNode.sourceNode)
608
+ continue;
609
+ for (const [targetId, targetNode] of allTargetNodes) {
522
610
  if (sourceId === targetId)
523
611
  continue;
524
612
  if (edgeEnhancement.detectionPattern(sourceNode, targetNode, this.parsedNodes, this.sharedContext)) {
@@ -538,7 +626,10 @@ export class TypeScriptParser {
538
626
  }
539
627
  }
540
628
  createFrameworkEdge(semanticType, relationshipType, sourceNodeId, targetNodeId, context = {}, relationshipWeight = 0.5) {
541
- const edgeId = `${semanticType}:${uuidv4()}`;
629
+ // Generate deterministic edge ID based on type + source + target
630
+ const edgeIdentity = `${semanticType}::${sourceNodeId}::${targetNodeId}`;
631
+ const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
632
+ const edgeId = `${semanticType}:${edgeHash}`;
542
633
  const properties = {
543
634
  coreType: semanticType, // This might need adjustment based on schema
544
635
  semanticType,
@@ -652,7 +743,6 @@ export class TypeScriptParser {
652
743
  const excludedPatterns = this.parseConfig.excludePatterns ?? [];
653
744
  for (const pattern of excludedPatterns) {
654
745
  if (filePath.includes(pattern) || filePath.match(new RegExp(pattern))) {
655
- console.log(`⏭️ Skipping excluded file: ${filePath}`);
656
746
  return true;
657
747
  }
658
748
  }
@@ -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,
@@ -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