code-graph-context 1.0.0 → 2.0.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 +221 -101
- package/dist/core/config/fairsquare-framework-schema.js +47 -60
- package/dist/core/config/nestjs-framework-schema.js +11 -1
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/timeouts.js +27 -0
- package/dist/core/embeddings/embeddings.service.js +122 -2
- package/dist/core/embeddings/natural-language-to-cypher.service.js +428 -30
- package/dist/core/parsers/parser-factory.js +6 -6
- package/dist/core/parsers/typescript-parser.js +639 -44
- package/dist/core/parsers/workspace-parser.js +553 -0
- package/dist/core/utils/edge-factory.js +37 -0
- package/dist/core/utils/file-change-detection.js +105 -0
- package/dist/core/utils/file-utils.js +20 -0
- package/dist/core/utils/index.js +3 -0
- package/dist/core/utils/path-utils.js +75 -0
- package/dist/core/utils/progress-reporter.js +112 -0
- package/dist/core/utils/project-id.js +176 -0
- package/dist/core/utils/retry.js +41 -0
- package/dist/core/workspace/index.js +4 -0
- package/dist/core/workspace/workspace-detector.js +221 -0
- package/dist/mcp/constants.js +172 -7
- package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
- package/dist/mcp/handlers/file-change-detection.js +105 -0
- package/dist/mcp/handlers/graph-generator.handler.js +97 -32
- package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
- package/dist/mcp/handlers/streaming-import.handler.js +210 -0
- package/dist/mcp/handlers/traversal.handler.js +130 -71
- package/dist/mcp/mcp.server.js +46 -7
- package/dist/mcp/service-init.js +79 -0
- package/dist/mcp/services/job-manager.js +165 -0
- package/dist/mcp/services/watch-manager.js +376 -0
- package/dist/mcp/services.js +48 -127
- package/dist/mcp/tools/check-parse-status.tool.js +64 -0
- package/dist/mcp/tools/impact-analysis.tool.js +319 -0
- package/dist/mcp/tools/index.js +15 -1
- package/dist/mcp/tools/list-projects.tool.js +62 -0
- package/dist/mcp/tools/list-watchers.tool.js +51 -0
- package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
- package/dist/mcp/tools/parse-typescript-project.tool.js +325 -60
- package/dist/mcp/tools/search-codebase.tool.js +57 -23
- package/dist/mcp/tools/start-watch-project.tool.js +100 -0
- package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
- package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
- package/dist/mcp/utils.js +35 -12
- package/dist/mcp/workers/parse-worker.js +198 -0
- package/dist/storage/neo4j/neo4j.service.js +273 -34
- package/package.json +4 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import neo4j from 'neo4j-driver';
|
|
2
2
|
import { MAX_TRAVERSAL_DEPTH } from '../../constants.js';
|
|
3
|
+
import { getTimeoutConfig } from '../../core/config/timeouts.js';
|
|
3
4
|
export class Neo4jService {
|
|
4
5
|
driver;
|
|
5
6
|
constructor() {
|
|
@@ -9,20 +10,39 @@ export class Neo4jService {
|
|
|
9
10
|
const uri = process.env.NEO4J_URI ?? 'bolt://localhost:7687';
|
|
10
11
|
const user = process.env.NEO4J_USER ?? 'neo4j';
|
|
11
12
|
const password = process.env.NEO4J_PASSWORD ?? 'PASSWORD';
|
|
12
|
-
|
|
13
|
+
const timeoutConfig = getTimeoutConfig();
|
|
14
|
+
return neo4j.driver(uri, neo4j.auth.basic(user, password), {
|
|
15
|
+
connectionTimeout: timeoutConfig.neo4j.connectionTimeoutMs,
|
|
16
|
+
maxTransactionRetryTime: timeoutConfig.neo4j.queryTimeoutMs,
|
|
17
|
+
});
|
|
13
18
|
}
|
|
14
19
|
async run(query, params = {}) {
|
|
15
20
|
const session = this.driver.session();
|
|
21
|
+
const timeoutConfig = getTimeoutConfig();
|
|
16
22
|
try {
|
|
17
|
-
const result = await session.run(query, params
|
|
23
|
+
const result = await session.run(query, params, {
|
|
24
|
+
timeout: timeoutConfig.neo4j.queryTimeoutMs,
|
|
25
|
+
});
|
|
18
26
|
return result.records.map((record) => record.toObject());
|
|
19
27
|
}
|
|
20
28
|
catch (error) {
|
|
29
|
+
// Provide helpful error message for timeout
|
|
30
|
+
if (error.code === 'Neo.TransientError.Transaction.Terminated') {
|
|
31
|
+
throw new Error(`Neo4j query timed out after ${timeoutConfig.neo4j.queryTimeoutMs}ms. ` +
|
|
32
|
+
'Consider simplifying the query or increasing NEO4J_QUERY_TIMEOUT_MS.');
|
|
33
|
+
}
|
|
21
34
|
console.error('Error running query:', error);
|
|
22
35
|
throw error;
|
|
23
36
|
}
|
|
24
37
|
finally {
|
|
25
|
-
|
|
38
|
+
// Wrap session close in try-catch to avoid masking the original error
|
|
39
|
+
try {
|
|
40
|
+
await session.close();
|
|
41
|
+
}
|
|
42
|
+
catch (closeError) {
|
|
43
|
+
// Log but don't re-throw to preserve original error
|
|
44
|
+
console.warn('Error closing Neo4j session:', closeError);
|
|
45
|
+
}
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
getDriver() {
|
|
@@ -30,15 +50,34 @@ export class Neo4jService {
|
|
|
30
50
|
}
|
|
31
51
|
async getSchema() {
|
|
32
52
|
const session = this.driver.session();
|
|
53
|
+
const timeoutConfig = getTimeoutConfig();
|
|
33
54
|
try {
|
|
34
|
-
return await session.run(QUERIES.APOC_SCHEMA
|
|
55
|
+
return await session.run(QUERIES.APOC_SCHEMA, {}, {
|
|
56
|
+
timeout: timeoutConfig.neo4j.queryTimeoutMs,
|
|
57
|
+
});
|
|
35
58
|
}
|
|
36
59
|
catch (error) {
|
|
37
60
|
console.error('Error fetching schema:', error);
|
|
38
61
|
throw error;
|
|
39
62
|
}
|
|
40
63
|
finally {
|
|
41
|
-
|
|
64
|
+
// Wrap session close in try-catch to avoid masking the original error
|
|
65
|
+
try {
|
|
66
|
+
await session.close();
|
|
67
|
+
}
|
|
68
|
+
catch (closeError) {
|
|
69
|
+
// Log but don't re-throw to preserve original error
|
|
70
|
+
console.warn('Error closing Neo4j session:', closeError);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Close the Neo4j driver connection.
|
|
76
|
+
* Should be called when the service is no longer needed to release resources.
|
|
77
|
+
*/
|
|
78
|
+
async close() {
|
|
79
|
+
if (this.driver) {
|
|
80
|
+
await this.driver.close();
|
|
42
81
|
}
|
|
43
82
|
}
|
|
44
83
|
}
|
|
@@ -47,7 +86,34 @@ export const QUERIES = {
|
|
|
47
86
|
CALL apoc.meta.schema() YIELD value
|
|
48
87
|
RETURN value as schema
|
|
49
88
|
`,
|
|
50
|
-
|
|
89
|
+
// Project-scoped deletion - only deletes nodes for the specified project
|
|
90
|
+
// Uses APOC batched deletion to avoid transaction memory limits on large projects
|
|
91
|
+
CLEAR_PROJECT: `
|
|
92
|
+
CALL apoc.periodic.iterate(
|
|
93
|
+
'MATCH (n) WHERE n.projectId = $projectId RETURN n',
|
|
94
|
+
'DETACH DELETE n',
|
|
95
|
+
{batchSize: 1000, params: {projectId: $projectId}}
|
|
96
|
+
)
|
|
97
|
+
YIELD batches, total
|
|
98
|
+
RETURN batches, total
|
|
99
|
+
`,
|
|
100
|
+
// Full database clear - use with caution, clears ALL projects
|
|
101
|
+
// Uses APOC batched deletion to avoid transaction memory limits
|
|
102
|
+
CLEAR_DATABASE: `
|
|
103
|
+
CALL apoc.periodic.iterate(
|
|
104
|
+
'MATCH (n) RETURN n',
|
|
105
|
+
'DETACH DELETE n',
|
|
106
|
+
{batchSize: 1000}
|
|
107
|
+
)
|
|
108
|
+
YIELD batches, total
|
|
109
|
+
RETURN batches, total
|
|
110
|
+
`,
|
|
111
|
+
// Create indexes on projectId for efficient filtering across key node types
|
|
112
|
+
CREATE_PROJECT_INDEX_EMBEDDED: 'CREATE INDEX project_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId)',
|
|
113
|
+
CREATE_PROJECT_INDEX_SOURCEFILE: 'CREATE INDEX project_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId)',
|
|
114
|
+
// Create composite indexes on projectId + id for efficient lookups
|
|
115
|
+
CREATE_PROJECT_ID_INDEX_EMBEDDED: 'CREATE INDEX project_id_embedded_idx IF NOT EXISTS FOR (n:Embedded) ON (n.projectId, n.id)',
|
|
116
|
+
CREATE_PROJECT_ID_INDEX_SOURCEFILE: 'CREATE INDEX project_id_sourcefile_idx IF NOT EXISTS FOR (n:SourceFile) ON (n.projectId, n.id)',
|
|
51
117
|
CREATE_NODE: `
|
|
52
118
|
UNWIND $nodes AS nodeData
|
|
53
119
|
CALL apoc.create.node(nodeData.labels, nodeData.properties) YIELD node
|
|
@@ -55,16 +121,17 @@ export const QUERIES = {
|
|
|
55
121
|
`,
|
|
56
122
|
CREATE_RELATIONSHIP: `
|
|
57
123
|
UNWIND $edges AS edgeData
|
|
58
|
-
MATCH (start) WHERE start.id = edgeData.startNodeId
|
|
59
|
-
MATCH (end) WHERE end.id = edgeData.endNodeId
|
|
124
|
+
MATCH (start) WHERE start.id = edgeData.startNodeId AND start.projectId = $projectId
|
|
125
|
+
MATCH (end) WHERE end.id = edgeData.endNodeId AND end.projectId = $projectId
|
|
60
126
|
WITH start, end, edgeData
|
|
61
127
|
CALL apoc.create.relationship(start, edgeData.type, edgeData.properties, end) YIELD rel
|
|
62
128
|
RETURN count(*) as created
|
|
63
129
|
`,
|
|
64
130
|
CREATE_INDEX: (label, property) => `CREATE INDEX IF NOT EXISTS FOR (n:${label}) ON (n.${property})`,
|
|
65
131
|
GET_STATS: `
|
|
66
|
-
MATCH (n)
|
|
67
|
-
|
|
132
|
+
MATCH (n)
|
|
133
|
+
WHERE n.projectId = $projectId
|
|
134
|
+
RETURN labels(n)[0] as nodeType, count(*) as count
|
|
68
135
|
ORDER BY count DESC
|
|
69
136
|
`,
|
|
70
137
|
CREATE_EMBEDDED_VECTOR_INDEX: `
|
|
@@ -75,9 +142,16 @@ export const QUERIES = {
|
|
|
75
142
|
\`vector.similarity_function\`: 'cosine'
|
|
76
143
|
}}
|
|
77
144
|
`,
|
|
145
|
+
// Vector search with configurable fetch multiplier for project filtering.
|
|
146
|
+
// fetchMultiplier (default: 10) controls how many extra results to fetch before filtering by projectId.
|
|
147
|
+
// minSimilarity (default: 0.3) filters out low-confidence matches for nonsense queries.
|
|
148
|
+
// Higher values = more accurate results but slower; lower values = faster but may miss results.
|
|
78
149
|
VECTOR_SEARCH: `
|
|
79
|
-
CALL db.index.vector.queryNodes('embedded_nodes_idx', $limit, $embedding)
|
|
150
|
+
CALL db.index.vector.queryNodes('embedded_nodes_idx', toInteger($limit * coalesce($fetchMultiplier, 10)), $embedding)
|
|
80
151
|
YIELD node, score
|
|
152
|
+
WHERE node.projectId = $projectId AND score >= coalesce($minSimilarity, 0.3)
|
|
153
|
+
WITH node, score
|
|
154
|
+
LIMIT toInteger($limit)
|
|
81
155
|
RETURN {
|
|
82
156
|
id: node.id,
|
|
83
157
|
labels: labels(node),
|
|
@@ -93,18 +167,22 @@ export const QUERIES = {
|
|
|
93
167
|
`,
|
|
94
168
|
GET_SOURCE_FILE_TRACKING_INFO: `
|
|
95
169
|
MATCH (sf:SourceFile)
|
|
96
|
-
|
|
170
|
+
WHERE sf.projectId = $projectId
|
|
171
|
+
RETURN sf.filePath AS filePath,
|
|
172
|
+
COALESCE(sf.mtime, 0) AS mtime,
|
|
173
|
+
COALESCE(sf.size, 0) AS size,
|
|
174
|
+
COALESCE(sf.contentHash, '') AS contentHash
|
|
97
175
|
`,
|
|
98
176
|
// Get cross-file edges before deletion (edges where one endpoint is outside the subgraph)
|
|
99
177
|
// These will be recreated after import using deterministic IDs
|
|
100
178
|
GET_CROSS_FILE_EDGES: `
|
|
101
179
|
MATCH (sf:SourceFile)
|
|
102
|
-
WHERE sf.filePath IN $filePaths
|
|
180
|
+
WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
|
|
103
181
|
OPTIONAL MATCH (sf)-[*]->(child)
|
|
104
182
|
WITH collect(DISTINCT sf) + collect(DISTINCT child) AS nodesToDelete
|
|
105
183
|
UNWIND nodesToDelete AS n
|
|
106
184
|
MATCH (n)-[r]-(other)
|
|
107
|
-
WHERE NOT other IN nodesToDelete
|
|
185
|
+
WHERE NOT other IN nodesToDelete AND other.projectId = $projectId
|
|
108
186
|
RETURN DISTINCT
|
|
109
187
|
startNode(r).id AS startNodeId,
|
|
110
188
|
endNode(r).id AS endNodeId,
|
|
@@ -114,7 +192,7 @@ export const QUERIES = {
|
|
|
114
192
|
// Delete source file subgraphs (nodes and all their edges)
|
|
115
193
|
DELETE_SOURCE_FILE_SUBGRAPHS: `
|
|
116
194
|
MATCH (sf:SourceFile)
|
|
117
|
-
WHERE sf.filePath IN $filePaths
|
|
195
|
+
WHERE sf.filePath IN $filePaths AND sf.projectId = $projectId
|
|
118
196
|
OPTIONAL MATCH (sf)-[*]->(child)
|
|
119
197
|
DETACH DELETE sf, child
|
|
120
198
|
`,
|
|
@@ -122,23 +200,21 @@ export const QUERIES = {
|
|
|
122
200
|
RECREATE_CROSS_FILE_EDGES: `
|
|
123
201
|
UNWIND $edges AS edge
|
|
124
202
|
MATCH (startNode {id: edge.startNodeId})
|
|
203
|
+
WHERE startNode.projectId = $projectId
|
|
125
204
|
MATCH (endNode {id: edge.endNodeId})
|
|
205
|
+
WHERE endNode.projectId = $projectId
|
|
126
206
|
CALL apoc.create.relationship(startNode, edge.edgeType, edge.edgeProperties, endNode) YIELD rel
|
|
127
207
|
RETURN count(rel) AS recreatedCount
|
|
128
208
|
`,
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
WHERE startNode(r) IS NULL OR endNode(r) IS NULL
|
|
134
|
-
DELETE r
|
|
135
|
-
RETURN count(r) AS deletedCount
|
|
136
|
-
`,
|
|
209
|
+
// Note: Dangling edge cleanup is not needed because:
|
|
210
|
+
// 1. DETACH DELETE removes all edges when deleting nodes
|
|
211
|
+
// 2. Edges cannot exist without both endpoints in Neo4j
|
|
212
|
+
// The previous query (WHERE startNode(r) IS NULL OR endNode(r) IS NULL) could never match anything
|
|
137
213
|
// Get existing nodes (excluding files being reparsed) for edge target matching
|
|
138
214
|
// Returns minimal info needed for edge detection: id, name, coreType, semanticType
|
|
139
215
|
GET_EXISTING_NODES_FOR_EDGE_DETECTION: `
|
|
140
216
|
MATCH (sf:SourceFile)-[*]->(n)
|
|
141
|
-
WHERE NOT sf.filePath IN $excludeFilePaths
|
|
217
|
+
WHERE NOT sf.filePath IN $excludeFilePaths AND sf.projectId = $projectId
|
|
142
218
|
RETURN n.id AS id,
|
|
143
219
|
n.name AS name,
|
|
144
220
|
n.coreType AS coreType,
|
|
@@ -149,29 +225,45 @@ export const QUERIES = {
|
|
|
149
225
|
EXPLORE_ALL_CONNECTIONS: (maxDepth = MAX_TRAVERSAL_DEPTH, direction = 'BOTH', relationshipTypes) => {
|
|
150
226
|
const safeMaxDepth = Math.min(Math.max(maxDepth, 1), MAX_TRAVERSAL_DEPTH);
|
|
151
227
|
// Build relationship pattern based on direction
|
|
228
|
+
// For INCOMING, we reverse the match order: (connected)-[*]->(start) instead of (start)<-[*]-(connected)
|
|
229
|
+
// This is because Neo4j variable-length patterns like <-[*1..N]- require ALL edges to point toward start,
|
|
230
|
+
// but in multi-hop paths (A→B→C), intermediate edges (A→B) don't point toward C, causing 0 results.
|
|
152
231
|
let relPattern = '';
|
|
232
|
+
let isReversed = false;
|
|
153
233
|
if (direction === 'OUTGOING') {
|
|
154
234
|
relPattern = `-[*1..${safeMaxDepth}]->`;
|
|
155
235
|
}
|
|
156
236
|
else if (direction === 'INCOMING') {
|
|
157
|
-
relPattern =
|
|
237
|
+
relPattern = `-[*1..${safeMaxDepth}]->`; // Same pattern as OUTGOING
|
|
238
|
+
isReversed = true; // But we'll reverse start/connected in MATCH
|
|
158
239
|
}
|
|
159
240
|
else {
|
|
160
241
|
relPattern = `-[*1..${safeMaxDepth}]-`;
|
|
161
242
|
}
|
|
162
243
|
// Build relationship type filter if specified
|
|
244
|
+
// SECURITY: Validate relationship types to prevent Cypher injection
|
|
245
|
+
// Only allow uppercase letters and underscores (valid Neo4j relationship type format)
|
|
163
246
|
let relTypeFilter = '';
|
|
164
247
|
if (relationshipTypes && relationshipTypes.length > 0) {
|
|
165
|
-
const
|
|
166
|
-
|
|
248
|
+
const validRelTypePattern = /^[A-Z_]+$/;
|
|
249
|
+
const validatedTypes = relationshipTypes.filter((t) => validRelTypePattern.test(t));
|
|
250
|
+
if (validatedTypes.length !== relationshipTypes.length) {
|
|
251
|
+
console.warn('Some relationship types were filtered out due to invalid format. Valid format: uppercase letters and underscores only.');
|
|
252
|
+
}
|
|
253
|
+
if (validatedTypes.length > 0) {
|
|
254
|
+
const types = validatedTypes.map((t) => `'${t}'`).join(', ');
|
|
255
|
+
relTypeFilter = `AND all(rel in relationships(path) WHERE type(rel) IN [${types}])`;
|
|
256
|
+
}
|
|
167
257
|
}
|
|
258
|
+
// For INCOMING, reverse the match: (connected)-[*]->(start) finds nodes that can REACH start
|
|
259
|
+
const matchPattern = isReversed ? `(connected)${relPattern}(start)` : `(start)${relPattern}(connected)`;
|
|
168
260
|
return `
|
|
169
|
-
MATCH (start) WHERE start.id = $nodeId
|
|
261
|
+
MATCH (start) WHERE start.id = $nodeId AND start.projectId = $projectId
|
|
170
262
|
|
|
171
263
|
CALL {
|
|
172
264
|
WITH start
|
|
173
|
-
MATCH path =
|
|
174
|
-
WHERE connected <> start
|
|
265
|
+
MATCH path = ${matchPattern}
|
|
266
|
+
WHERE connected <> start AND connected.projectId = $projectId
|
|
175
267
|
${relTypeFilter}
|
|
176
268
|
WITH path, connected, length(path) as depth
|
|
177
269
|
|
|
@@ -267,13 +359,13 @@ export const QUERIES = {
|
|
|
267
359
|
return `
|
|
268
360
|
// Unwind the source nodes we're exploring from
|
|
269
361
|
UNWIND $sourceNodeIds AS sourceId
|
|
270
|
-
MATCH (source) WHERE source.id = sourceId
|
|
362
|
+
MATCH (source) WHERE source.id = sourceId AND source.projectId = $projectId
|
|
271
363
|
|
|
272
364
|
// Find immediate neighbors (exactly 1 hop)
|
|
273
365
|
MATCH (source)${relPattern}(neighbor)
|
|
274
366
|
|
|
275
|
-
// Filter: skip already visited nodes
|
|
276
|
-
WHERE NOT neighbor.id IN $visitedNodeIds
|
|
367
|
+
// Filter: skip already visited nodes and ensure same project
|
|
368
|
+
WHERE NOT neighbor.id IN $visitedNodeIds AND neighbor.projectId = $projectId
|
|
277
369
|
|
|
278
370
|
// Calculate the three scoring components
|
|
279
371
|
WITH source, neighbor, rel,
|
|
@@ -326,7 +418,154 @@ export const QUERIES = {
|
|
|
326
418
|
|
|
327
419
|
// Sort by score and limit to top N per depth
|
|
328
420
|
ORDER BY combinedScore DESC
|
|
329
|
-
LIMIT ${maxNodesPerDepth}
|
|
421
|
+
LIMIT toInteger(${maxNodesPerDepth})
|
|
330
422
|
`;
|
|
331
423
|
},
|
|
424
|
+
// ============================================
|
|
425
|
+
// DYNAMIC SCHEMA DISCOVERY QUERIES
|
|
426
|
+
// ============================================
|
|
427
|
+
/**
|
|
428
|
+
* Get all distinct node labels with counts and sample properties
|
|
429
|
+
*/
|
|
430
|
+
DISCOVER_NODE_TYPES: `
|
|
431
|
+
CALL db.labels() YIELD label
|
|
432
|
+
CALL {
|
|
433
|
+
WITH label
|
|
434
|
+
MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
|
|
435
|
+
WITH n LIMIT 1
|
|
436
|
+
RETURN keys(n) AS sampleProperties
|
|
437
|
+
}
|
|
438
|
+
CALL {
|
|
439
|
+
WITH label
|
|
440
|
+
MATCH (n) WHERE label IN labels(n) AND n.projectId = $projectId
|
|
441
|
+
RETURN count(n) AS nodeCount
|
|
442
|
+
}
|
|
443
|
+
RETURN label, nodeCount, sampleProperties
|
|
444
|
+
ORDER BY nodeCount DESC
|
|
445
|
+
`,
|
|
446
|
+
/**
|
|
447
|
+
* Get all distinct relationship types with counts and which node types they connect
|
|
448
|
+
*/
|
|
449
|
+
DISCOVER_RELATIONSHIP_TYPES: `
|
|
450
|
+
CALL db.relationshipTypes() YIELD relationshipType
|
|
451
|
+
CALL {
|
|
452
|
+
WITH relationshipType
|
|
453
|
+
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId AND b.projectId = $projectId
|
|
454
|
+
WITH labels(a)[0] AS fromLabel, labels(b)[0] AS toLabel
|
|
455
|
+
RETURN fromLabel, toLabel
|
|
456
|
+
LIMIT 10
|
|
457
|
+
}
|
|
458
|
+
CALL {
|
|
459
|
+
WITH relationshipType
|
|
460
|
+
MATCH (a)-[r]->(b) WHERE type(r) = relationshipType AND a.projectId = $projectId
|
|
461
|
+
RETURN count(r) AS relCount
|
|
462
|
+
}
|
|
463
|
+
RETURN relationshipType, relCount, collect(DISTINCT {from: fromLabel, to: toLabel}) AS connections
|
|
464
|
+
ORDER BY relCount DESC
|
|
465
|
+
`,
|
|
466
|
+
/**
|
|
467
|
+
* Get sample nodes of each semantic type for context
|
|
468
|
+
*/
|
|
469
|
+
DISCOVER_SEMANTIC_TYPES: `
|
|
470
|
+
MATCH (n)
|
|
471
|
+
WHERE n.semanticType IS NOT NULL AND n.projectId = $projectId
|
|
472
|
+
WITH n.semanticType AS semanticType, count(*) AS count
|
|
473
|
+
ORDER BY count DESC
|
|
474
|
+
RETURN semanticType, count
|
|
475
|
+
`,
|
|
476
|
+
/**
|
|
477
|
+
* Get example query patterns based on actual graph structure
|
|
478
|
+
*/
|
|
479
|
+
DISCOVER_COMMON_PATTERNS: `
|
|
480
|
+
MATCH (a)-[r]->(b)
|
|
481
|
+
WHERE a.projectId = $projectId AND b.projectId = $projectId
|
|
482
|
+
WITH labels(a)[0] AS fromType, type(r) AS relType, labels(b)[0] AS toType, count(*) AS count
|
|
483
|
+
WHERE count > 5
|
|
484
|
+
RETURN fromType, relType, toType, count
|
|
485
|
+
ORDER BY count DESC
|
|
486
|
+
LIMIT 20
|
|
487
|
+
`,
|
|
488
|
+
// ============================================
|
|
489
|
+
// IMPACT ANALYSIS QUERIES
|
|
490
|
+
// Reuses cross-file edge pattern to find dependents
|
|
491
|
+
// ============================================
|
|
492
|
+
/**
|
|
493
|
+
* Get node details by ID
|
|
494
|
+
*/
|
|
495
|
+
GET_NODE_BY_ID: `
|
|
496
|
+
MATCH (n) WHERE n.id = $nodeId AND n.projectId = $projectId
|
|
497
|
+
RETURN n.id AS id,
|
|
498
|
+
n.name AS name,
|
|
499
|
+
labels(n) AS labels,
|
|
500
|
+
n.semanticType AS semanticType,
|
|
501
|
+
n.coreType AS coreType,
|
|
502
|
+
n.filePath AS filePath
|
|
503
|
+
`,
|
|
504
|
+
/**
|
|
505
|
+
* Get impact of changing a node - finds all external nodes that depend on it
|
|
506
|
+
* Based on GET_CROSS_FILE_EDGES pattern but for a single node
|
|
507
|
+
*/
|
|
508
|
+
GET_NODE_IMPACT: `
|
|
509
|
+
MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
|
|
510
|
+
MATCH (dependent)-[r]->(target)
|
|
511
|
+
WHERE dependent.id <> target.id AND dependent.projectId = $projectId
|
|
512
|
+
RETURN DISTINCT
|
|
513
|
+
dependent.id AS nodeId,
|
|
514
|
+
dependent.name AS name,
|
|
515
|
+
labels(dependent) AS labels,
|
|
516
|
+
dependent.semanticType AS semanticType,
|
|
517
|
+
dependent.coreType AS coreType,
|
|
518
|
+
dependent.filePath AS filePath,
|
|
519
|
+
type(r) AS relationshipType,
|
|
520
|
+
coalesce(r.relationshipWeight, 0.5) AS weight
|
|
521
|
+
`,
|
|
522
|
+
/**
|
|
523
|
+
* Get impact of changing a file - finds all external nodes that depend on nodes in this file
|
|
524
|
+
* Directly reuses GET_CROSS_FILE_EDGES pattern
|
|
525
|
+
*/
|
|
526
|
+
GET_FILE_IMPACT: `
|
|
527
|
+
MATCH (sf:SourceFile)
|
|
528
|
+
WHERE sf.projectId = $projectId
|
|
529
|
+
AND (sf.filePath = $filePath OR sf.filePath ENDS WITH '/' + $filePath)
|
|
530
|
+
MATCH (sf)-[:CONTAINS]->(entity)
|
|
531
|
+
WHERE entity:Class OR entity:Function OR entity:Interface
|
|
532
|
+
WITH collect(DISTINCT entity) AS entitiesInFile, sf.filePath AS sourceFilePath
|
|
533
|
+
UNWIND entitiesInFile AS n
|
|
534
|
+
MATCH (dependent)-[r]->(n)
|
|
535
|
+
WHERE NOT dependent IN entitiesInFile
|
|
536
|
+
AND dependent.projectId = $projectId
|
|
537
|
+
AND dependent.filePath <> sourceFilePath
|
|
538
|
+
RETURN DISTINCT
|
|
539
|
+
dependent.id AS nodeId,
|
|
540
|
+
dependent.name AS name,
|
|
541
|
+
labels(dependent) AS labels,
|
|
542
|
+
dependent.semanticType AS semanticType,
|
|
543
|
+
dependent.coreType AS coreType,
|
|
544
|
+
dependent.filePath AS filePath,
|
|
545
|
+
type(r) AS relationshipType,
|
|
546
|
+
coalesce(r.relationshipWeight, 0.5) AS weight,
|
|
547
|
+
n.id AS targetNodeId,
|
|
548
|
+
n.name AS targetNodeName
|
|
549
|
+
`,
|
|
550
|
+
/**
|
|
551
|
+
* Get transitive dependents - nodes that depend on dependents (for deeper impact)
|
|
552
|
+
*/
|
|
553
|
+
GET_TRANSITIVE_DEPENDENTS: (maxDepth = 4) => `
|
|
554
|
+
MATCH (target) WHERE target.id = $nodeId AND target.projectId = $projectId
|
|
555
|
+
MATCH path = (dependent)-[*2..${maxDepth}]->(target)
|
|
556
|
+
WHERE dependent.projectId = $projectId AND all(n IN nodes(path) WHERE n.projectId = $projectId)
|
|
557
|
+
WITH dependent,
|
|
558
|
+
length(path) AS depth,
|
|
559
|
+
[r IN relationships(path) | type(r)] AS relationshipPath
|
|
560
|
+
RETURN DISTINCT
|
|
561
|
+
dependent.id AS nodeId,
|
|
562
|
+
dependent.name AS name,
|
|
563
|
+
labels(dependent) AS labels,
|
|
564
|
+
dependent.semanticType AS semanticType,
|
|
565
|
+
dependent.coreType AS coreType,
|
|
566
|
+
dependent.filePath AS filePath,
|
|
567
|
+
depth,
|
|
568
|
+
relationshipPath
|
|
569
|
+
ORDER BY depth ASC
|
|
570
|
+
`,
|
|
332
571
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-graph-context",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MCP server that builds code graphs to provide rich context to LLMs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/drewdrewH/code-graph-context#readme",
|
|
@@ -49,12 +49,15 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
52
|
+
"@parcel/watcher": "^2.5.1",
|
|
52
53
|
"commander": "^14.0.0",
|
|
53
54
|
"dotenv": "^17.2.3",
|
|
54
55
|
"glob": "^11.0.3",
|
|
55
56
|
"neo4j": "^2.0.0-RC2",
|
|
56
57
|
"neo4j-driver": "^5.28.1",
|
|
57
58
|
"openai": "^5.10.1",
|
|
59
|
+
"ts-morph": "^26.0.0",
|
|
60
|
+
"yaml": "^2.8.2",
|
|
58
61
|
"zod": "^3.25.76"
|
|
59
62
|
},
|
|
60
63
|
"devDependencies": {
|
|
@@ -73,7 +76,6 @@
|
|
|
73
76
|
"globals": "^16.2.0",
|
|
74
77
|
"prettier": "^3.5.3",
|
|
75
78
|
"reflect-metadata": "^0.2.2",
|
|
76
|
-
"ts-morph": "^26.0.0",
|
|
77
79
|
"ts-node": "^10.9.2",
|
|
78
80
|
"typescript": "^5.8.3",
|
|
79
81
|
"typescript-eslint": "^8.34.1",
|