code-graph-context 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/constants.js +21 -0
- package/dist/core/config/nestjs-framework-schema.js +2 -1
- package/dist/core/config/schema.js +2 -1
- package/dist/core/embeddings/natural-language-to-cypher.service.js +12 -13
- package/dist/core/parsers/parser-factory.js +3 -2
- package/dist/core/parsers/typescript-parser.js +129 -39
- package/dist/mcp/constants.js +19 -2
- package/dist/mcp/mcp.server.js +1 -1
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/impact-analysis.tool.js +253 -0
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/parse-typescript-project.tool.js +127 -42
- package/dist/mcp/tools/search-codebase.tool.js +4 -10
- package/dist/mcp/utils.js +4 -17
- package/dist/storage/neo4j/neo4j.service.js +201 -6
- package/dist/utils/file-utils.js +20 -0
- package/package.json +2 -1
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* Parses TypeScript/NestJS projects and builds Neo4j graph
|
|
4
4
|
*/
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
|
-
import {
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
@@ -2,22 +2,8 @@
|
|
|
2
2
|
* MCP Server Utility Functions
|
|
3
3
|
* Common utility functions used across the MCP server
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
|
|
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 =
|
|
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": "
|
|
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"
|