code-graph-context 1.1.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.
Files changed (47) hide show
  1. package/README.md +221 -101
  2. package/dist/core/config/fairsquare-framework-schema.js +47 -60
  3. package/dist/core/config/nestjs-framework-schema.js +11 -1
  4. package/dist/core/config/schema.js +1 -1
  5. package/dist/core/config/timeouts.js +27 -0
  6. package/dist/core/embeddings/embeddings.service.js +122 -2
  7. package/dist/core/embeddings/natural-language-to-cypher.service.js +416 -17
  8. package/dist/core/parsers/parser-factory.js +5 -3
  9. package/dist/core/parsers/typescript-parser.js +614 -45
  10. package/dist/core/parsers/workspace-parser.js +553 -0
  11. package/dist/core/utils/edge-factory.js +37 -0
  12. package/dist/core/utils/file-change-detection.js +105 -0
  13. package/dist/core/utils/file-utils.js +20 -0
  14. package/dist/core/utils/index.js +3 -0
  15. package/dist/core/utils/path-utils.js +75 -0
  16. package/dist/core/utils/progress-reporter.js +112 -0
  17. package/dist/core/utils/project-id.js +176 -0
  18. package/dist/core/utils/retry.js +41 -0
  19. package/dist/core/workspace/index.js +4 -0
  20. package/dist/core/workspace/workspace-detector.js +221 -0
  21. package/dist/mcp/constants.js +153 -5
  22. package/dist/mcp/handlers/cross-file-edge.helpers.js +19 -0
  23. package/dist/mcp/handlers/file-change-detection.js +105 -0
  24. package/dist/mcp/handlers/graph-generator.handler.js +97 -32
  25. package/dist/mcp/handlers/incremental-parse.handler.js +146 -0
  26. package/dist/mcp/handlers/streaming-import.handler.js +210 -0
  27. package/dist/mcp/handlers/traversal.handler.js +130 -71
  28. package/dist/mcp/mcp.server.js +45 -6
  29. package/dist/mcp/service-init.js +79 -0
  30. package/dist/mcp/services/job-manager.js +165 -0
  31. package/dist/mcp/services/watch-manager.js +376 -0
  32. package/dist/mcp/services.js +2 -2
  33. package/dist/mcp/tools/check-parse-status.tool.js +64 -0
  34. package/dist/mcp/tools/impact-analysis.tool.js +84 -18
  35. package/dist/mcp/tools/index.js +13 -1
  36. package/dist/mcp/tools/list-projects.tool.js +62 -0
  37. package/dist/mcp/tools/list-watchers.tool.js +51 -0
  38. package/dist/mcp/tools/natural-language-to-cypher.tool.js +34 -8
  39. package/dist/mcp/tools/parse-typescript-project.tool.js +318 -58
  40. package/dist/mcp/tools/search-codebase.tool.js +56 -16
  41. package/dist/mcp/tools/start-watch-project.tool.js +100 -0
  42. package/dist/mcp/tools/stop-watch-project.tool.js +49 -0
  43. package/dist/mcp/tools/traverse-from-node.tool.js +68 -9
  44. package/dist/mcp/utils.js +35 -13
  45. package/dist/mcp/workers/parse-worker.js +198 -0
  46. package/dist/storage/neo4j/neo4j.service.js +147 -48
  47. 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
+ };