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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Parser
|
|
3
|
+
* Orchestrates parsing of multi-package monorepos
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { glob } from 'glob';
|
|
8
|
+
import { debugLog } from '../utils/file-utils.js';
|
|
9
|
+
import { CORE_TYPESCRIPT_SCHEMA, } from '../config/schema.js';
|
|
10
|
+
import { createFrameworkEdgeData } from '../utils/edge-factory.js';
|
|
11
|
+
import { resolveProjectId } from '../utils/project-id.js';
|
|
12
|
+
import { ParserFactory } from './parser-factory.js';
|
|
13
|
+
export class WorkspaceParser {
|
|
14
|
+
config;
|
|
15
|
+
projectId;
|
|
16
|
+
projectType;
|
|
17
|
+
lazyLoad;
|
|
18
|
+
discoveredFiles = null;
|
|
19
|
+
parsedNodes = new Map();
|
|
20
|
+
parsedEdges = new Map();
|
|
21
|
+
accumulatedDeferredEdges = [];
|
|
22
|
+
// Shared context across all packages for cross-package edge detection
|
|
23
|
+
sharedContext = new Map();
|
|
24
|
+
// Lightweight node copies for cross-package edge detection (no AST references)
|
|
25
|
+
accumulatedParsedNodes = new Map();
|
|
26
|
+
// Framework schemas detected from packages (for edge enhancements)
|
|
27
|
+
frameworkSchemas = [];
|
|
28
|
+
constructor(config, projectId, lazyLoad = true, projectType = 'auto') {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.projectId = resolveProjectId(config.rootPath, projectId);
|
|
31
|
+
this.lazyLoad = lazyLoad;
|
|
32
|
+
this.projectType = projectType;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the project ID for this workspace
|
|
36
|
+
*/
|
|
37
|
+
getProjectId() {
|
|
38
|
+
return this.projectId;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get workspace configuration
|
|
42
|
+
*/
|
|
43
|
+
getConfig() {
|
|
44
|
+
return this.config;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Discover all source files across all packages
|
|
48
|
+
*/
|
|
49
|
+
async discoverSourceFiles() {
|
|
50
|
+
if (this.discoveredFiles !== null) {
|
|
51
|
+
// Return flattened list
|
|
52
|
+
return Array.from(this.discoveredFiles.values()).flat();
|
|
53
|
+
}
|
|
54
|
+
this.discoveredFiles = new Map();
|
|
55
|
+
let totalFiles = 0;
|
|
56
|
+
const packageCounts = {};
|
|
57
|
+
for (const pkg of this.config.packages) {
|
|
58
|
+
const files = await this.discoverPackageFiles(pkg);
|
|
59
|
+
this.discoveredFiles.set(pkg.name, files);
|
|
60
|
+
totalFiles += files.length;
|
|
61
|
+
packageCounts[pkg.name] = files.length;
|
|
62
|
+
}
|
|
63
|
+
await debugLog('WorkspaceParser discovered files', {
|
|
64
|
+
totalFiles,
|
|
65
|
+
packageCount: this.config.packages.length,
|
|
66
|
+
packageCounts,
|
|
67
|
+
});
|
|
68
|
+
return Array.from(this.discoveredFiles.values()).flat();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Discover files in a single package
|
|
72
|
+
*/
|
|
73
|
+
async discoverPackageFiles(pkg) {
|
|
74
|
+
// Include both .ts and .tsx files
|
|
75
|
+
const pattern = path.join(pkg.path, '**/*.{ts,tsx}');
|
|
76
|
+
const files = await glob(pattern, {
|
|
77
|
+
ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/build/**'],
|
|
78
|
+
absolute: true,
|
|
79
|
+
});
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get files grouped by package
|
|
84
|
+
*/
|
|
85
|
+
async getFilesByPackage() {
|
|
86
|
+
if (this.discoveredFiles === null) {
|
|
87
|
+
await this.discoverSourceFiles();
|
|
88
|
+
}
|
|
89
|
+
return this.discoveredFiles;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create a parser for a package using ParserFactory (supports auto-detection)
|
|
93
|
+
* Injects the shared context so context is shared across all packages.
|
|
94
|
+
*/
|
|
95
|
+
async createParserForPackage(pkg) {
|
|
96
|
+
const tsConfigPath = pkg.tsConfigPath || path.join(pkg.path, 'tsconfig.json');
|
|
97
|
+
let parser;
|
|
98
|
+
if (this.projectType === 'auto') {
|
|
99
|
+
// Auto-detect framework for this specific package
|
|
100
|
+
parser = await ParserFactory.createParserWithAutoDetection(pkg.path, tsConfigPath, this.projectId, this.lazyLoad);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Use the specified project type for all packages
|
|
104
|
+
parser = ParserFactory.createParser({
|
|
105
|
+
workspacePath: pkg.path,
|
|
106
|
+
tsConfigPath,
|
|
107
|
+
projectType: this.projectType,
|
|
108
|
+
projectId: this.projectId,
|
|
109
|
+
lazyLoad: this.lazyLoad,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Inject shared context so all packages share the same context
|
|
113
|
+
// This enables cross-package edge detection (e.g., INTERNAL_API_CALL)
|
|
114
|
+
parser.setSharedContext(this.sharedContext);
|
|
115
|
+
// Defer edge enhancements to WorkspaceParser's final pass
|
|
116
|
+
// This avoids duplicate work and enables cross-package edge detection
|
|
117
|
+
parser.setDeferEdgeEnhancements(true);
|
|
118
|
+
return parser;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Parse a single package and return its results
|
|
122
|
+
*/
|
|
123
|
+
async parsePackage(pkg) {
|
|
124
|
+
console.log(`\nParsing package: ${pkg.name}`);
|
|
125
|
+
const parser = await this.createParserForPackage(pkg);
|
|
126
|
+
// Discover files for this package
|
|
127
|
+
const files = await this.discoverPackageFiles(pkg);
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
console.log(` ⚠️ No TypeScript files found in ${pkg.name}`);
|
|
130
|
+
return { nodes: [], edges: [] };
|
|
131
|
+
}
|
|
132
|
+
console.log(` 📄 ${files.length} files to parse`);
|
|
133
|
+
// Parse all files in this package
|
|
134
|
+
const result = await parser.parseChunk(files, true); // Skip edge resolution for now
|
|
135
|
+
// Add package name to all nodes
|
|
136
|
+
for (const node of result.nodes) {
|
|
137
|
+
node.properties.packageName = pkg.name;
|
|
138
|
+
}
|
|
139
|
+
console.log(` ✅ ${result.nodes.length} nodes, ${result.edges.length} edges`);
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Parse a chunk of files (for streaming compatibility)
|
|
144
|
+
* Files are grouped by package and parsed together
|
|
145
|
+
*/
|
|
146
|
+
async parseChunk(filePaths, skipEdgeResolution = false) {
|
|
147
|
+
// Group files by package
|
|
148
|
+
const filesByPackage = new Map();
|
|
149
|
+
for (const filePath of filePaths) {
|
|
150
|
+
const pkg = this.findPackageForFile(filePath);
|
|
151
|
+
if (pkg) {
|
|
152
|
+
const files = filesByPackage.get(pkg) ?? [];
|
|
153
|
+
files.push(filePath);
|
|
154
|
+
filesByPackage.set(pkg, files);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const allNodes = [];
|
|
158
|
+
const allEdges = [];
|
|
159
|
+
// Parse each package's files
|
|
160
|
+
for (const [pkg, files] of filesByPackage) {
|
|
161
|
+
try {
|
|
162
|
+
const parser = await this.createParserForPackage(pkg);
|
|
163
|
+
const result = await parser.parseChunk(files, skipEdgeResolution);
|
|
164
|
+
// Add package name to nodes
|
|
165
|
+
for (const node of result.nodes) {
|
|
166
|
+
node.properties.packageName = pkg.name;
|
|
167
|
+
}
|
|
168
|
+
// Export and accumulate deferred edges for cross-package resolution
|
|
169
|
+
const chunkData = parser.exportChunkResults();
|
|
170
|
+
this.accumulatedDeferredEdges.push(...chunkData.deferredEdges);
|
|
171
|
+
// Accumulate LIGHTWEIGHT copies of ParsedNodes for cross-package edge detection
|
|
172
|
+
// Only stores what's needed for detection patterns - NO AST references
|
|
173
|
+
const innerParsedNodes = parser.getParsedNodes();
|
|
174
|
+
for (const [nodeId, parsedNode] of innerParsedNodes) {
|
|
175
|
+
this.accumulatedParsedNodes.set(nodeId, {
|
|
176
|
+
id: parsedNode.id,
|
|
177
|
+
semanticType: parsedNode.semanticType,
|
|
178
|
+
properties: {
|
|
179
|
+
name: parsedNode.properties.name,
|
|
180
|
+
context: parsedNode.properties.context, // Contains propertyTypes
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Accumulate framework schemas (deduplicated by name)
|
|
185
|
+
for (const schema of parser.getFrameworkSchemas()) {
|
|
186
|
+
if (!this.frameworkSchemas.some((s) => s.name === schema.name)) {
|
|
187
|
+
this.frameworkSchemas.push(schema);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
allNodes.push(...result.nodes);
|
|
191
|
+
allEdges.push(...result.edges);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.warn(`⚠️ Failed to parse package ${pkg.name}:`, error);
|
|
195
|
+
// Continue with other packages
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { nodes: allNodes, edges: allEdges };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Find which package a file belongs to
|
|
202
|
+
*/
|
|
203
|
+
findPackageForFile(filePath) {
|
|
204
|
+
for (const pkg of this.config.packages) {
|
|
205
|
+
if (filePath.startsWith(pkg.path)) {
|
|
206
|
+
return pkg;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Parse all packages in the workspace
|
|
213
|
+
*/
|
|
214
|
+
async parseAll() {
|
|
215
|
+
const packageResults = new Map();
|
|
216
|
+
const allNodes = [];
|
|
217
|
+
const allEdges = [];
|
|
218
|
+
for (const pkg of this.config.packages) {
|
|
219
|
+
const result = await this.parsePackage(pkg);
|
|
220
|
+
allNodes.push(...result.nodes);
|
|
221
|
+
allEdges.push(...result.edges);
|
|
222
|
+
packageResults.set(pkg.name, {
|
|
223
|
+
nodes: result.nodes.length,
|
|
224
|
+
edges: result.edges.length,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
console.log(`\n🎉 Workspace parsing complete!`);
|
|
228
|
+
console.log(` Total: ${allNodes.length} nodes, ${allEdges.length} edges`);
|
|
229
|
+
return {
|
|
230
|
+
nodes: allNodes,
|
|
231
|
+
edges: allEdges,
|
|
232
|
+
packageResults,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Clear parsed data (for memory management)
|
|
237
|
+
* Note: Does NOT clear accumulated deferred edges - those need to be resolved at the end
|
|
238
|
+
*/
|
|
239
|
+
clearParsedData() {
|
|
240
|
+
this.parsedNodes.clear();
|
|
241
|
+
this.parsedEdges.clear();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Add existing nodes for cross-package edge resolution
|
|
245
|
+
*/
|
|
246
|
+
addExistingNodesFromChunk(nodes) {
|
|
247
|
+
for (const node of nodes) {
|
|
248
|
+
this.parsedNodes.set(node.id, node);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get current counts for progress reporting
|
|
253
|
+
*/
|
|
254
|
+
getCurrentCounts() {
|
|
255
|
+
return {
|
|
256
|
+
nodes: this.parsedNodes.size,
|
|
257
|
+
edges: this.parsedEdges.size,
|
|
258
|
+
deferredEdges: this.accumulatedDeferredEdges.length,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Resolve accumulated deferred edges against all parsed nodes
|
|
263
|
+
* Call this after all chunks have been parsed
|
|
264
|
+
*/
|
|
265
|
+
async resolveDeferredEdgesManually() {
|
|
266
|
+
const resolvedEdges = [];
|
|
267
|
+
const unresolvedImports = [];
|
|
268
|
+
const unresolvedExtends = [];
|
|
269
|
+
const unresolvedImplements = [];
|
|
270
|
+
// Count by edge type for logging
|
|
271
|
+
const importsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'IMPORTS').length;
|
|
272
|
+
const extendsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'EXTENDS').length;
|
|
273
|
+
const implementsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'IMPLEMENTS').length;
|
|
274
|
+
for (const deferred of this.accumulatedDeferredEdges) {
|
|
275
|
+
// Find target node by name, type, and optionally file path from accumulated nodes
|
|
276
|
+
const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
|
|
277
|
+
if (targetNode) {
|
|
278
|
+
// Find source node to get filePath
|
|
279
|
+
const sourceNode = this.parsedNodes.get(deferred.sourceNodeId);
|
|
280
|
+
const filePath = sourceNode?.properties.filePath ?? '';
|
|
281
|
+
// Get relationship weight from core schema
|
|
282
|
+
const coreEdgeType = deferred.edgeType;
|
|
283
|
+
const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[coreEdgeType];
|
|
284
|
+
const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
|
|
285
|
+
// Generate a unique edge ID
|
|
286
|
+
const edgeHash = crypto
|
|
287
|
+
.createHash('md5')
|
|
288
|
+
.update(`${deferred.sourceNodeId}-${deferred.edgeType}-${targetNode.id}`)
|
|
289
|
+
.digest('hex')
|
|
290
|
+
.substring(0, 12);
|
|
291
|
+
const edge = {
|
|
292
|
+
id: `${this.projectId}:${deferred.edgeType}:${edgeHash}`,
|
|
293
|
+
type: deferred.edgeType,
|
|
294
|
+
startNodeId: deferred.sourceNodeId,
|
|
295
|
+
endNodeId: targetNode.id,
|
|
296
|
+
properties: {
|
|
297
|
+
coreType: coreEdgeType,
|
|
298
|
+
projectId: this.projectId,
|
|
299
|
+
source: 'ast',
|
|
300
|
+
confidence: 1.0,
|
|
301
|
+
relationshipWeight,
|
|
302
|
+
filePath,
|
|
303
|
+
createdAt: new Date().toISOString(),
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
resolvedEdges.push(edge);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Track unresolved by type
|
|
310
|
+
if (deferred.edgeType === 'IMPORTS') {
|
|
311
|
+
unresolvedImports.push(deferred.targetName);
|
|
312
|
+
}
|
|
313
|
+
else if (deferred.edgeType === 'EXTENDS') {
|
|
314
|
+
unresolvedExtends.push(deferred.targetName);
|
|
315
|
+
}
|
|
316
|
+
else if (deferred.edgeType === 'IMPLEMENTS') {
|
|
317
|
+
unresolvedImplements.push(deferred.targetName);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Log resolution stats
|
|
322
|
+
const importsResolved = resolvedEdges.filter((e) => e.type === 'IMPORTS').length;
|
|
323
|
+
const extendsResolved = resolvedEdges.filter((e) => e.type === 'EXTENDS').length;
|
|
324
|
+
const implementsResolved = resolvedEdges.filter((e) => e.type === 'IMPLEMENTS').length;
|
|
325
|
+
debugLog('WorkspaceParser edge resolution', {
|
|
326
|
+
totalDeferredEdges: this.accumulatedDeferredEdges.length,
|
|
327
|
+
totalNodesAvailable: this.parsedNodes.size,
|
|
328
|
+
imports: {
|
|
329
|
+
queued: importsCount,
|
|
330
|
+
resolved: importsResolved,
|
|
331
|
+
unresolved: unresolvedImports.length,
|
|
332
|
+
sample: unresolvedImports.slice(0, 10),
|
|
333
|
+
},
|
|
334
|
+
extends: {
|
|
335
|
+
queued: extendsCount,
|
|
336
|
+
resolved: extendsResolved,
|
|
337
|
+
unresolved: unresolvedExtends.length,
|
|
338
|
+
sample: unresolvedExtends.slice(0, 10),
|
|
339
|
+
},
|
|
340
|
+
implements: {
|
|
341
|
+
queued: implementsCount,
|
|
342
|
+
resolved: implementsResolved,
|
|
343
|
+
unresolved: unresolvedImplements.length,
|
|
344
|
+
sample: unresolvedImplements.slice(0, 10),
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
// Clear accumulated deferred edges after resolution
|
|
348
|
+
this.accumulatedDeferredEdges = [];
|
|
349
|
+
return resolvedEdges;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Apply edge enhancements on all accumulated nodes across all packages.
|
|
353
|
+
* This enables cross-package edge detection (e.g., INTERNAL_API_CALL between services and
|
|
354
|
+
* vendor controllers in different packages).
|
|
355
|
+
*
|
|
356
|
+
* Uses shared context and accumulated ParsedNodes from all packages.
|
|
357
|
+
* @returns New edges created by edge enhancements
|
|
358
|
+
*/
|
|
359
|
+
async applyEdgeEnhancementsManually() {
|
|
360
|
+
if (this.accumulatedParsedNodes.size === 0) {
|
|
361
|
+
console.log('WorkspaceParser: No accumulated nodes for edge enhancements');
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
if (this.frameworkSchemas.length === 0) {
|
|
365
|
+
console.log('WorkspaceParser: No framework schemas for edge enhancements');
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
console.log(`WorkspaceParser: Applying edge enhancements on ${this.accumulatedParsedNodes.size} accumulated nodes across all packages...`);
|
|
369
|
+
// Pre-index nodes by semantic type for O(1) lookups
|
|
370
|
+
const nodesBySemanticType = new Map();
|
|
371
|
+
for (const [nodeId, node] of this.accumulatedParsedNodes) {
|
|
372
|
+
const semanticType = node.semanticType || 'unknown';
|
|
373
|
+
if (!nodesBySemanticType.has(semanticType)) {
|
|
374
|
+
nodesBySemanticType.set(semanticType, new Map());
|
|
375
|
+
}
|
|
376
|
+
nodesBySemanticType.get(semanticType).set(nodeId, node);
|
|
377
|
+
}
|
|
378
|
+
const typeCounts = {};
|
|
379
|
+
for (const [type, nodes] of nodesBySemanticType) {
|
|
380
|
+
typeCounts[type] = nodes.size;
|
|
381
|
+
}
|
|
382
|
+
console.log(`Node distribution by semantic type:`, typeCounts);
|
|
383
|
+
const newEdges = [];
|
|
384
|
+
const edgeCountBefore = this.parsedEdges.size;
|
|
385
|
+
// Apply edge enhancements from all framework schemas
|
|
386
|
+
for (const frameworkSchema of this.frameworkSchemas) {
|
|
387
|
+
for (const edgeEnhancement of Object.values(frameworkSchema.edgeEnhancements)) {
|
|
388
|
+
const enhancementEdges = await this.applyEdgeEnhancement(edgeEnhancement, nodesBySemanticType);
|
|
389
|
+
newEdges.push(...enhancementEdges);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const newEdgeCount = this.parsedEdges.size - edgeCountBefore;
|
|
393
|
+
console.log(`Created ${newEdgeCount} cross-package edges from edge enhancements`);
|
|
394
|
+
return newEdges;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Apply a single edge enhancement across all accumulated parsed nodes.
|
|
398
|
+
* Uses LightweightParsedNode which contains only fields needed for detection:
|
|
399
|
+
* - id, semanticType, properties.context (with propertyTypes)
|
|
400
|
+
* Detection patterns must NOT access sourceNode (AST) - use properties instead.
|
|
401
|
+
*/
|
|
402
|
+
async applyEdgeEnhancement(edgeEnhancement, _nodesBySemanticType) {
|
|
403
|
+
const newEdges = [];
|
|
404
|
+
// Track created edges with simple key to avoid duplicate hash computations
|
|
405
|
+
const createdEdgeKeys = new Set();
|
|
406
|
+
try {
|
|
407
|
+
// For now, iterate all nodes. Detection pattern short-circuits on semantic type.
|
|
408
|
+
// Future optimization: use _nodesBySemanticType to only iterate relevant pairs.
|
|
409
|
+
const allTargetNodes = new Map([...this.accumulatedParsedNodes]);
|
|
410
|
+
for (const [sourceId, sourceNode] of this.accumulatedParsedNodes) {
|
|
411
|
+
for (const [targetId, targetNode] of allTargetNodes) {
|
|
412
|
+
if (sourceId === targetId)
|
|
413
|
+
continue;
|
|
414
|
+
// Run detection pattern FIRST (cheap semantic type checks)
|
|
415
|
+
if (edgeEnhancement.detectionPattern(sourceNode, targetNode, this.accumulatedParsedNodes, this.sharedContext)) {
|
|
416
|
+
const simpleKey = `${sourceId}:${targetId}`;
|
|
417
|
+
if (createdEdgeKeys.has(simpleKey))
|
|
418
|
+
continue;
|
|
419
|
+
createdEdgeKeys.add(simpleKey);
|
|
420
|
+
// Extract context for this edge
|
|
421
|
+
let context = {};
|
|
422
|
+
if (edgeEnhancement.contextExtractor) {
|
|
423
|
+
context = edgeEnhancement.contextExtractor(sourceNode, targetNode, this.accumulatedParsedNodes, this.sharedContext);
|
|
424
|
+
}
|
|
425
|
+
const edge = this.createFrameworkEdge(edgeEnhancement.semanticType, edgeEnhancement.neo4j.relationshipType, sourceId, targetId, context, edgeEnhancement.relationshipWeight);
|
|
426
|
+
this.parsedEdges.set(edge.id, edge);
|
|
427
|
+
newEdges.push(edge);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.error(`Error applying edge enhancement ${edgeEnhancement.name}:`, error);
|
|
434
|
+
}
|
|
435
|
+
return newEdges;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Create a framework edge with semantic type and properties.
|
|
439
|
+
*/
|
|
440
|
+
createFrameworkEdge(semanticType, relationshipType, sourceId, targetId, context, relationshipWeight) {
|
|
441
|
+
const { id, properties } = createFrameworkEdgeData({
|
|
442
|
+
semanticType,
|
|
443
|
+
sourceNodeId: sourceId,
|
|
444
|
+
targetNodeId: targetId,
|
|
445
|
+
projectId: this.projectId,
|
|
446
|
+
context,
|
|
447
|
+
relationshipWeight,
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
id,
|
|
451
|
+
type: relationshipType,
|
|
452
|
+
startNodeId: sourceId,
|
|
453
|
+
endNodeId: targetId,
|
|
454
|
+
properties,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Find a node by name and type from accumulated nodes
|
|
459
|
+
* For SourceFiles, implements smart import resolution:
|
|
460
|
+
* - Direct file path match
|
|
461
|
+
* - Relative import resolution (./foo, ../bar)
|
|
462
|
+
* - Scoped package imports (@workspace/ui, @ui/core)
|
|
463
|
+
*
|
|
464
|
+
* For ClassDeclaration/InterfaceDeclaration with filePath, uses precise matching.
|
|
465
|
+
*/
|
|
466
|
+
findNodeByNameAndType(name, type, filePath) {
|
|
467
|
+
const allNodes = [...this.parsedNodes.values()];
|
|
468
|
+
// If we have a file path and it's not a SourceFile, use precise matching first
|
|
469
|
+
if (filePath && type !== 'SourceFile') {
|
|
470
|
+
for (const node of allNodes) {
|
|
471
|
+
if (node.properties.coreType === type &&
|
|
472
|
+
node.properties.name === name &&
|
|
473
|
+
node.properties.filePath === filePath) {
|
|
474
|
+
return node;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// If precise match fails, fall through to name-only matching below
|
|
478
|
+
}
|
|
479
|
+
// For SOURCE_FILE with import specifier, try multiple matching strategies
|
|
480
|
+
if (type === 'SourceFile') {
|
|
481
|
+
// Strategy 1: Direct file path match
|
|
482
|
+
for (const node of allNodes) {
|
|
483
|
+
if (node.labels.includes(type) && node.properties.filePath === name) {
|
|
484
|
+
return node;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Strategy 2: Resolve relative imports (./foo, ../bar)
|
|
488
|
+
if (name.startsWith('.')) {
|
|
489
|
+
// Normalize: remove leading ./ or ../
|
|
490
|
+
const normalizedPath = name.replace(/^\.\.\//, '').replace(/^\.\//, '');
|
|
491
|
+
// Try matching with common extensions
|
|
492
|
+
const extensions = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'];
|
|
493
|
+
for (const ext of extensions) {
|
|
494
|
+
const searchPath = normalizedPath + ext;
|
|
495
|
+
for (const node of allNodes) {
|
|
496
|
+
if (node.labels.includes(type)) {
|
|
497
|
+
// Match if filePath ends with the normalized path
|
|
498
|
+
if (node.properties.filePath.endsWith(searchPath) ||
|
|
499
|
+
node.properties.filePath.endsWith('/' + searchPath)) {
|
|
500
|
+
return node;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Strategy 3: Workspace package imports (@workspace/ui, @ui/core)
|
|
507
|
+
if (name.startsWith('@')) {
|
|
508
|
+
const parts = name.split('/');
|
|
509
|
+
const packageName = parts.slice(0, 2).join('/'); // @scope/package
|
|
510
|
+
const subPath = parts.slice(2).join('/'); // rest of path after package name
|
|
511
|
+
// First, try to find an exact match with subpath
|
|
512
|
+
if (subPath) {
|
|
513
|
+
const extensions = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'];
|
|
514
|
+
for (const ext of extensions) {
|
|
515
|
+
const searchPath = subPath + ext;
|
|
516
|
+
for (const node of allNodes) {
|
|
517
|
+
if (node.labels.includes(type) && node.properties.packageName === packageName) {
|
|
518
|
+
if (node.properties.filePath.endsWith(searchPath) ||
|
|
519
|
+
node.properties.filePath.endsWith('/' + searchPath)) {
|
|
520
|
+
return node;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// For bare package imports (@workspace/ui), look for index files
|
|
527
|
+
if (!subPath) {
|
|
528
|
+
for (const node of allNodes) {
|
|
529
|
+
if (node.labels.includes(type) && node.properties.packageName === packageName) {
|
|
530
|
+
const fileName = node.properties.name;
|
|
531
|
+
if (fileName === 'index.ts' || fileName === 'index.tsx') {
|
|
532
|
+
return node;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// If no index file, return any file from the package as a fallback
|
|
537
|
+
for (const node of allNodes) {
|
|
538
|
+
if (node.labels.includes(type) && node.properties.packageName === packageName) {
|
|
539
|
+
return node;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Default: exact name match (for non-SourceFile types like classes, interfaces)
|
|
546
|
+
for (const node of allNodes) {
|
|
547
|
+
if (node.properties.coreType === type && node.properties.name === name) {
|
|
548
|
+
return node;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge Factory
|
|
3
|
+
* Shared utilities for creating framework edges with consistent ID generation and properties
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Generate a deterministic edge ID based on semantic type, source, and target.
|
|
8
|
+
* Uses SHA256 hash truncated to 16 characters for uniqueness.
|
|
9
|
+
*/
|
|
10
|
+
export const generateFrameworkEdgeId = (semanticType, sourceNodeId, targetNodeId) => {
|
|
11
|
+
const edgeIdentity = `${semanticType}::${sourceNodeId}::${targetNodeId}`;
|
|
12
|
+
const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
|
|
13
|
+
return `${semanticType}:${edgeHash}`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create framework edge ID and properties.
|
|
17
|
+
* Returns common edge data that can be used to construct either ParsedEdge or Neo4jEdge.
|
|
18
|
+
*
|
|
19
|
+
* @param params - Edge parameters
|
|
20
|
+
* @returns Edge ID and properties object
|
|
21
|
+
*/
|
|
22
|
+
export const createFrameworkEdgeData = (params) => {
|
|
23
|
+
const { semanticType, sourceNodeId, targetNodeId, projectId, context = {}, relationshipWeight = 0.5 } = params;
|
|
24
|
+
const id = generateFrameworkEdgeId(semanticType, sourceNodeId, targetNodeId);
|
|
25
|
+
const properties = {
|
|
26
|
+
coreType: semanticType,
|
|
27
|
+
projectId,
|
|
28
|
+
semanticType,
|
|
29
|
+
source: 'pattern',
|
|
30
|
+
confidence: 0.8,
|
|
31
|
+
relationshipWeight,
|
|
32
|
+
filePath: '',
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
context,
|
|
35
|
+
};
|
|
36
|
+
return { id, properties };
|
|
37
|
+
};
|