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.
@@ -3,12 +3,16 @@
3
3
  * Parses TypeScript/NestJS projects and builds Neo4j graph
4
4
  */
5
5
  import { writeFileSync } from 'fs';
6
- import { join } from 'path';
6
+ import { stat } from 'fs/promises';
7
+ import { join, resolve } from 'path';
8
+ import { glob } from 'glob';
7
9
  import { z } from 'zod';
10
+ import { EXCLUDE_PATTERNS_GLOB } from '../../constants.js';
8
11
  import { CORE_TYPESCRIPT_SCHEMA } from '../../core/config/schema.js';
9
12
  import { EmbeddingsService } from '../../core/embeddings/embeddings.service.js';
10
13
  import { ParserFactory } from '../../core/parsers/parser-factory.js';
11
- import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
14
+ import { Neo4jService, QUERIES } from '../../storage/neo4j/neo4j.service.js';
15
+ import { hashFile } from '../../utils/file-utils.js';
12
16
  import { TOOL_NAMES, TOOL_METADATA, DEFAULTS, FILE_PATHS, LOG_CONFIG } from '../constants.js';
13
17
  import { GraphGeneratorHandler } from '../handlers/graph-generator.handler.js';
14
18
  import { createErrorResponse, createSuccessResponse, formatParseSuccess, formatParsePartialSuccess, debugLog, } from '../utils.js';
@@ -38,58 +42,41 @@ export const createParseTypescriptProjectTool = (server) => {
38
42
  clearExisting,
39
43
  projectType,
40
44
  });
41
- // Create parser with auto-detection or specified type
42
- let parser;
43
- if (projectType === 'auto') {
44
- parser = await ParserFactory.createParserWithAutoDetection(projectPath, tsconfigPath);
45
- }
46
- else {
47
- parser = ParserFactory.createParser({
48
- workspacePath: projectPath,
49
- tsConfigPath: tsconfigPath,
50
- projectType: projectType,
51
- });
52
- }
53
- // Parse the workspace
54
- const { nodes, edges } = await parser.parseWorkspace();
55
- const { nodes: cleanNodes, edges: cleanEdges } = parser.exportToJson();
56
- console.log(`Parsed ${cleanNodes.length} nodes / ${cleanEdges.length} edges`);
57
- await debugLog('Parsing completed', {
58
- nodeCount: cleanNodes.length,
59
- edgeCount: cleanEdges.length,
45
+ const neo4jService = new Neo4jService();
46
+ const embeddingsService = new EmbeddingsService();
47
+ const graphGeneratorHandler = new GraphGeneratorHandler(neo4jService, embeddingsService);
48
+ const graphData = await parseProject({
49
+ neo4jService,
50
+ tsconfigPath,
51
+ projectPath,
52
+ clearExisting,
53
+ projectType,
60
54
  });
61
- // Create graph JSON output
55
+ const { nodes, edges, savedCrossFileEdges } = graphData;
56
+ console.log(`Parsed ${nodes.length} nodes / ${edges.length} edges`);
57
+ await debugLog('Parsing completed', { nodeCount: nodes.length, edgeCount: edges.length });
62
58
  const outputPath = join(projectPath, FILE_PATHS.graphOutput);
63
- // Get detected framework schemas from parser
64
- const frameworkSchemas = parser['frameworkSchemas']?.map((s) => s.name) ?? ['Auto-detected'];
65
- const graphData = {
66
- nodes: cleanNodes,
67
- edges: cleanEdges,
68
- metadata: {
69
- coreSchema: CORE_TYPESCRIPT_SCHEMA.name,
70
- frameworkSchemas,
71
- projectType,
72
- generated: new Date().toISOString(),
73
- },
74
- };
75
59
  writeFileSync(outputPath, JSON.stringify(graphData, null, LOG_CONFIG.jsonIndentation));
76
60
  console.log(`Graph data written to ${outputPath}`);
77
- // Attempt to import to Neo4j
78
61
  try {
79
- const neo4jService = new Neo4jService();
80
- const embeddingsService = new EmbeddingsService();
81
- const graphGeneratorHandler = new GraphGeneratorHandler(neo4jService, embeddingsService);
82
62
  const result = await graphGeneratorHandler.generateGraph(outputPath, DEFAULTS.batchSize, clearExisting);
63
+ // Recreate cross-file edges after incremental parse
64
+ if (!clearExisting && savedCrossFileEdges.length > 0) {
65
+ await debugLog('Recreating cross-file edges', { edgesToRecreate: savedCrossFileEdges.length });
66
+ const recreateResult = await neo4jService.run(QUERIES.RECREATE_CROSS_FILE_EDGES, {
67
+ edges: savedCrossFileEdges,
68
+ });
69
+ const recreatedCount = recreateResult[0]?.recreatedCount ?? 0;
70
+ await debugLog('Cross-file edges recreated', { recreatedCount, expected: savedCrossFileEdges.length });
71
+ }
83
72
  console.log('Graph generation completed:', result);
84
73
  await debugLog('Neo4j import completed', result);
85
- const successMessage = formatParseSuccess(cleanNodes.length, cleanEdges.length, result);
86
- return createSuccessResponse(successMessage);
74
+ return createSuccessResponse(formatParseSuccess(nodes.length, edges.length, result));
87
75
  }
88
76
  catch (neo4jError) {
89
77
  console.error('Neo4j import failed:', neo4jError);
90
78
  await debugLog('Neo4j import failed', neo4jError);
91
- const partialSuccessMessage = formatParsePartialSuccess(cleanNodes.length, cleanEdges.length, outputPath, neo4jError.message);
92
- return createSuccessResponse(partialSuccessMessage);
79
+ return createSuccessResponse(formatParsePartialSuccess(nodes.length, edges.length, outputPath, neo4jError.message));
93
80
  }
94
81
  }
95
82
  catch (error) {
@@ -99,3 +86,101 @@ export const createParseTypescriptProjectTool = (server) => {
99
86
  }
100
87
  });
101
88
  };
89
+ const parseProject = async (options) => {
90
+ const { neo4jService, tsconfigPath, projectPath, clearExisting = true, projectType = 'auto' } = options;
91
+ const parser = projectType === 'auto'
92
+ ? await ParserFactory.createParserWithAutoDetection(projectPath, tsconfigPath)
93
+ : ParserFactory.createParser({
94
+ workspacePath: projectPath,
95
+ tsConfigPath: tsconfigPath,
96
+ projectType: projectType,
97
+ });
98
+ let incrementalStats;
99
+ let savedCrossFileEdges = [];
100
+ if (clearExisting) {
101
+ // Full rebuild: parse all files
102
+ await parser.parseWorkspace();
103
+ }
104
+ else {
105
+ // Incremental: detect changes and parse only affected files
106
+ const { filesToReparse, filesToDelete } = await detectChangedFiles(projectPath, neo4jService);
107
+ incrementalStats = { filesReparsed: filesToReparse.length, filesDeleted: filesToDelete.length };
108
+ await debugLog('Incremental change detection', { filesToReparse, filesToDelete });
109
+ const filesToRemoveFromGraph = [...filesToDelete, ...filesToReparse];
110
+ if (filesToRemoveFromGraph.length > 0) {
111
+ // Save cross-file edges before deletion (they'll be recreated after import)
112
+ savedCrossFileEdges = await getCrossFileEdges(neo4jService, filesToRemoveFromGraph);
113
+ await debugLog('Saved cross-file edges', { count: savedCrossFileEdges.length, edges: savedCrossFileEdges });
114
+ await deleteSourceFileSubgraphs(neo4jService, filesToRemoveFromGraph);
115
+ }
116
+ if (filesToReparse.length > 0) {
117
+ await debugLog('Incremental parse starting', {
118
+ filesChanged: filesToReparse.length,
119
+ filesDeleted: filesToDelete.length,
120
+ });
121
+ // Load existing nodes from Neo4j for edge target matching
122
+ const existingNodes = await loadExistingNodesForEdgeDetection(neo4jService, filesToRemoveFromGraph);
123
+ await debugLog('Loaded existing nodes for edge detection', { count: existingNodes.length });
124
+ parser.setExistingNodes(existingNodes);
125
+ await parser.parseWorkspace(filesToReparse);
126
+ }
127
+ else {
128
+ await debugLog('Incremental parse: no changes detected');
129
+ }
130
+ }
131
+ const { nodes, edges } = parser.exportToJson();
132
+ const frameworkSchemas = parser['frameworkSchemas']?.map((s) => s.name) ?? ['Auto-detected'];
133
+ return {
134
+ nodes,
135
+ edges,
136
+ savedCrossFileEdges,
137
+ metadata: {
138
+ coreSchema: CORE_TYPESCRIPT_SCHEMA.name,
139
+ frameworkSchemas,
140
+ projectType,
141
+ generated: new Date().toISOString(),
142
+ ...(incrementalStats && { incremental: incrementalStats }),
143
+ },
144
+ };
145
+ };
146
+ const deleteSourceFileSubgraphs = async (neo4jService, filePaths) => {
147
+ await neo4jService.run(QUERIES.DELETE_SOURCE_FILE_SUBGRAPHS, { filePaths });
148
+ };
149
+ const loadExistingNodesForEdgeDetection = async (neo4jService, excludeFilePaths) => {
150
+ const queryResult = await neo4jService.run(QUERIES.GET_EXISTING_NODES_FOR_EDGE_DETECTION, { excludeFilePaths });
151
+ return queryResult;
152
+ };
153
+ const getCrossFileEdges = async (neo4jService, filePaths) => {
154
+ const queryResult = await neo4jService.run(QUERIES.GET_CROSS_FILE_EDGES, { filePaths });
155
+ return queryResult;
156
+ };
157
+ const detectChangedFiles = async (projectPath, neo4jService) => {
158
+ const relativeFiles = await glob('**/*.ts', { cwd: projectPath, ignore: EXCLUDE_PATTERNS_GLOB });
159
+ const currentFiles = new Set(relativeFiles.map((f) => resolve(projectPath, f)));
160
+ const queryResult = await neo4jService.run(QUERIES.GET_SOURCE_FILE_TRACKING_INFO);
161
+ const indexedFiles = queryResult;
162
+ const indexedMap = new Map(indexedFiles.map((f) => [f.filePath, f]));
163
+ const filesToReparse = [];
164
+ const filesToDelete = [];
165
+ for (const absolutePath of currentFiles) {
166
+ const indexed = indexedMap.get(absolutePath);
167
+ if (!indexed) {
168
+ filesToReparse.push(absolutePath);
169
+ continue;
170
+ }
171
+ const fileStats = await stat(absolutePath);
172
+ if (fileStats.mtimeMs === indexed.mtime && fileStats.size === indexed.size) {
173
+ continue;
174
+ }
175
+ const currentHash = await hashFile(absolutePath);
176
+ if (currentHash !== indexed.contentHash) {
177
+ filesToReparse.push(absolutePath);
178
+ }
179
+ }
180
+ for (const indexedPath of indexedMap.keys()) {
181
+ if (!currentFiles.has(indexedPath)) {
182
+ filesToDelete.push(indexedPath);
183
+ }
184
+ }
185
+ return { filesToReparse, filesToDelete };
186
+ };
@@ -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, limit = DEFAULTS.searchLimit, maxDepth = DEFAULTS.traversalDepth, maxNodesPerChain = 5, skip = 0, includeCode = true, snippetLength = DEFAULTS.codeSnippetLength, useWeightedTraversal = true, }) => {
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, limit });
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: parseInt(limit.toString()),
55
+ limit: 1,
62
56
  embedding,
63
57
  });
64
58
  if (vectorResults.length === 0) {
65
- await debugLog('No relevant code found', { query, limit });
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
@@ -2,22 +2,8 @@
2
2
  * MCP Server Utility Functions
3
3
  * Common utility functions used across the MCP server
4
4
  */
5
- import fs from 'fs/promises';
6
- import path from 'path';
7
- import { FILE_PATHS, LOG_CONFIG, MESSAGES } from './constants.js';
8
- /**
9
- * Debug logging utility
10
- */
11
- export const debugLog = async (message, data) => {
12
- const timestamp = new Date().toISOString();
13
- const logEntry = `[${timestamp}] ${message}\n${data ? JSON.stringify(data, null, LOG_CONFIG.jsonIndentation) : ''}\n${LOG_CONFIG.logSeparator}\n`;
14
- try {
15
- await fs.appendFile(path.join(process.cwd(), FILE_PATHS.debugLog), logEntry);
16
- }
17
- catch (error) {
18
- console.error('Failed to write debug log:', error);
19
- }
20
- };
5
+ import { MESSAGES } from './constants.js';
6
+ export { debugLog } from '../utils/file-utils.js';
21
7
  /**
22
8
  * Standard error response format for MCP tools
23
9
  */
@@ -69,7 +55,8 @@ export const formatNodeInfo = (value, key) => {
69
55
  else {
70
56
  // Show first 500 and last 500 characters
71
57
  const half = Math.floor(maxLength / 2);
72
- result.sourceCode = code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
58
+ result.sourceCode =
59
+ code.substring(0, half) + '\n\n... [truncated] ...\n\n' + code.substring(code.length - half);
73
60
  result.hasMore = true;
74
61
  result.truncated = code.length - maxLength;
75
62
  }
@@ -81,15 +81,70 @@ export const QUERIES = {
81
81
  RETURN {
82
82
  id: node.id,
83
83
  labels: labels(node),
84
- properties: apoc.map.removeKeys(properties(node), ['embedding'])
84
+ properties: apoc.map.removeKeys(properties(node), ['embedding', 'contentHash', 'mtime', 'size'])
85
85
  } as node, score
86
86
  ORDER BY score DESC
87
87
  `,
88
88
  // Check if index exists
89
89
  CHECK_VECTOR_INDEX: `
90
- SHOW INDEXES YIELD name, type
90
+ SHOW INDEXES YIELD name, type
91
91
  WHERE name = 'node_embedding_idx' AND type = 'VECTOR'
92
92
  RETURN count(*) > 0 as exists
93
+ `,
94
+ GET_SOURCE_FILE_TRACKING_INFO: `
95
+ MATCH (sf:SourceFile)
96
+ RETURN sf.filePath AS filePath, sf.mtime AS mtime, sf.size AS size, sf.contentHash AS contentHash
97
+ `,
98
+ // Get cross-file edges before deletion (edges where one endpoint is outside the subgraph)
99
+ // These will be recreated after import using deterministic IDs
100
+ GET_CROSS_FILE_EDGES: `
101
+ MATCH (sf:SourceFile)
102
+ WHERE sf.filePath IN $filePaths
103
+ OPTIONAL MATCH (sf)-[*]->(child)
104
+ WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesToDelete
105
+ UNWIND nodesToDelete AS n
106
+ MATCH (n)-[r]-(other)
107
+ WHERE NOT other IN nodesToDelete
108
+ RETURN DISTINCT
109
+ startNode(r).id AS startNodeId,
110
+ endNode(r).id AS endNodeId,
111
+ type(r) AS edgeType,
112
+ properties(r) AS edgeProperties
113
+ `,
114
+ // Delete source file subgraphs (nodes and all their edges)
115
+ DELETE_SOURCE_FILE_SUBGRAPHS: `
116
+ MATCH (sf:SourceFile)
117
+ WHERE sf.filePath IN $filePaths
118
+ OPTIONAL MATCH (sf)-[*]->(child)
119
+ DETACH DELETE sf, child
120
+ `,
121
+ // Recreate cross-file edges after import (uses deterministic IDs)
122
+ RECREATE_CROSS_FILE_EDGES: `
123
+ UNWIND $edges AS edge
124
+ MATCH (startNode {id: edge.startNodeId})
125
+ MATCH (endNode {id: edge.endNodeId})
126
+ CALL apoc.create.relationship(startNode, edge.edgeType, edge.edgeProperties, endNode) YIELD rel
127
+ RETURN count(rel) AS recreatedCount
128
+ `,
129
+ // Clean up dangling edges (edges pointing to non-existent nodes)
130
+ // Run after incremental parse to remove edges to renamed/deleted nodes
131
+ CLEANUP_DANGLING_EDGES: `
132
+ MATCH ()-[r]->()
133
+ WHERE startNode(r) IS NULL OR endNode(r) IS NULL
134
+ DELETE r
135
+ RETURN count(r) AS deletedCount
136
+ `,
137
+ // Get existing nodes (excluding files being reparsed) for edge target matching
138
+ // Returns minimal info needed for edge detection: id, name, coreType, semanticType
139
+ GET_EXISTING_NODES_FOR_EDGE_DETECTION: `
140
+ MATCH (sf:SourceFile)-[*]->(n)
141
+ WHERE NOT sf.filePath IN $excludeFilePaths
142
+ RETURN n.id AS id,
143
+ n.name AS name,
144
+ n.coreType AS coreType,
145
+ n.semanticType AS semanticType,
146
+ labels(n) AS labels,
147
+ sf.filePath AS filePath
93
148
  `,
94
149
  EXPLORE_ALL_CONNECTIONS: (maxDepth = MAX_TRAVERSAL_DEPTH, direction = 'BOTH', relationshipTypes) => {
95
150
  const safeMaxDepth = Math.min(Math.max(maxDepth, 1), MAX_TRAVERSAL_DEPTH);
@@ -123,7 +178,7 @@ export const QUERIES = {
123
178
  RETURN {
124
179
  id: connected.id,
125
180
  labels: labels(connected),
126
- properties: apoc.map.removeKeys(properties(connected), ['embedding'])
181
+ properties: apoc.map.removeKeys(properties(connected), ['embedding', 'contentHash', 'mtime', 'size'])
127
182
  } as node,
128
183
  depth,
129
184
  [rel in relationships(path) | {
@@ -148,7 +203,7 @@ export const QUERIES = {
148
203
  startNode: {
149
204
  id: start.id,
150
205
  labels: labels(start),
151
- properties: apoc.map.removeKeys(properties(start), ['embedding'])
206
+ properties: apoc.map.removeKeys(properties(start), ['embedding', 'contentHash', 'mtime', 'size'])
152
207
  },
153
208
  connections: connections,
154
209
  totalConnections: size(allConnections),
@@ -156,7 +211,7 @@ export const QUERIES = {
156
211
  nodes: [conn in connections | conn.node] + [{
157
212
  id: start.id,
158
213
  labels: labels(start),
159
- properties: apoc.map.removeKeys(properties(start), ['embedding'])
214
+ properties: apoc.map.removeKeys(properties(start), ['embedding', 'contentHash', 'mtime', 'size'])
160
215
  }],
161
216
  relationships: reduce(rels = [], conn in connections | rels + conn.relationshipChain)
162
217
  }
@@ -252,7 +307,7 @@ export const QUERIES = {
252
307
  node: {
253
308
  id: neighbor.id,
254
309
  labels: labels(neighbor),
255
- properties: apoc.map.removeKeys(properties(neighbor), ['embedding'])
310
+ properties: apoc.map.removeKeys(properties(neighbor), ['embedding', 'contentHash', 'mtime', 'size'])
256
311
  },
257
312
  relationship: {
258
313
  type: type(rel),
@@ -274,4 +329,144 @@ export const QUERIES = {
274
329
  LIMIT ${maxNodesPerDepth}
275
330
  `;
276
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
+ `,
277
472
  };
@@ -0,0 +1,20 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ const DEBUG_LOG_FILE = 'debug-search.log';
5
+ const LOG_SEPARATOR = '---';
6
+ const JSON_INDENT = 2;
7
+ export const hashFile = async (filePath) => {
8
+ const content = await fs.readFile(filePath);
9
+ return crypto.createHash('sha256').update(content).digest('hex');
10
+ };
11
+ export const debugLog = async (message, data) => {
12
+ const timestamp = new Date().toISOString();
13
+ const logEntry = `[${timestamp}] ${message}\n${data ? JSON.stringify(data, null, JSON_INDENT) : ''}\n${LOG_SEPARATOR}\n`;
14
+ try {
15
+ await fs.appendFile(path.join(process.cwd(), DEBUG_LOG_FILE), logEntry);
16
+ }
17
+ catch (error) {
18
+ console.error('Failed to write debug log:', error);
19
+ }
20
+ };
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
+ "homepage": "https://github.com/drewdrewH/code-graph-context#readme",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/drewdrewH/code-graph-context.git"