code-graph-context 2.0.1 → 2.2.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 (33) hide show
  1. package/README.md +156 -2
  2. package/dist/constants.js +167 -0
  3. package/dist/core/config/fairsquare-framework-schema.js +9 -7
  4. package/dist/core/config/schema.js +41 -2
  5. package/dist/core/embeddings/natural-language-to-cypher.service.js +166 -110
  6. package/dist/core/parsers/typescript-parser.js +1039 -742
  7. package/dist/core/parsers/workspace-parser.js +175 -193
  8. package/dist/core/utils/code-normalizer.js +299 -0
  9. package/dist/core/utils/file-change-detection.js +17 -2
  10. package/dist/core/utils/file-utils.js +40 -5
  11. package/dist/core/utils/graph-factory.js +161 -0
  12. package/dist/core/utils/shared-utils.js +79 -0
  13. package/dist/core/workspace/workspace-detector.js +59 -5
  14. package/dist/mcp/constants.js +141 -8
  15. package/dist/mcp/handlers/graph-generator.handler.js +1 -0
  16. package/dist/mcp/handlers/incremental-parse.handler.js +3 -6
  17. package/dist/mcp/handlers/parallel-import.handler.js +136 -0
  18. package/dist/mcp/handlers/streaming-import.handler.js +14 -59
  19. package/dist/mcp/mcp.server.js +1 -1
  20. package/dist/mcp/services/job-manager.js +5 -8
  21. package/dist/mcp/services/watch-manager.js +7 -18
  22. package/dist/mcp/tools/detect-dead-code.tool.js +413 -0
  23. package/dist/mcp/tools/detect-duplicate-code.tool.js +450 -0
  24. package/dist/mcp/tools/impact-analysis.tool.js +20 -4
  25. package/dist/mcp/tools/index.js +4 -0
  26. package/dist/mcp/tools/parse-typescript-project.tool.js +15 -14
  27. package/dist/mcp/workers/chunk-worker-pool.js +196 -0
  28. package/dist/mcp/workers/chunk-worker.types.js +4 -0
  29. package/dist/mcp/workers/chunk.worker.js +89 -0
  30. package/dist/mcp/workers/parse-coordinator.js +183 -0
  31. package/dist/mcp/workers/worker.pool.js +54 -0
  32. package/dist/storage/neo4j/neo4j.service.js +190 -10
  33. package/package.json +1 -1
@@ -2,14 +2,16 @@
2
2
  * Workspace Parser
3
3
  * Orchestrates parsing of multi-package monorepos
4
4
  */
5
- import crypto from 'crypto';
6
5
  import path from 'path';
7
6
  import { glob } from 'glob';
7
+ import { EXCLUDE_PATTERNS_GLOB } from '../../constants.js';
8
+ import { FAIRSQUARE_FRAMEWORK_SCHEMA } from '../config/fairsquare-framework-schema.js';
9
+ import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
8
10
  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 { createFrameworkEdgeData } from '../utils/graph-factory.js';
11
12
  import { resolveProjectId } from '../utils/project-id.js';
12
13
  import { ParserFactory } from './parser-factory.js';
14
+ import { TypeScriptParser } from './typescript-parser.js';
13
15
  export class WorkspaceParser {
14
16
  config;
15
17
  projectId;
@@ -25,6 +27,11 @@ export class WorkspaceParser {
25
27
  accumulatedParsedNodes = new Map();
26
28
  // Framework schemas detected from packages (for edge enhancements)
27
29
  frameworkSchemas = [];
30
+ // Track already exported items to avoid returning duplicates in streaming mode
31
+ exportedNodeIds = new Set();
32
+ exportedEdgeIds = new Set();
33
+ // Resolver parser for delegating edge resolution to TypeScriptParser
34
+ resolverParser = null;
28
35
  constructor(config, projectId, lazyLoad = true, projectType = 'auto') {
29
36
  this.config = config;
30
37
  this.projectId = resolveProjectId(config.rootPath, projectId);
@@ -72,9 +79,10 @@ export class WorkspaceParser {
72
79
  */
73
80
  async discoverPackageFiles(pkg) {
74
81
  // Include both .ts and .tsx files
82
+ // Use EXCLUDE_PATTERNS_GLOB for consistency with detectChangedFiles and TypeScriptParser
75
83
  const pattern = path.join(pkg.path, '**/*.{ts,tsx}');
76
84
  const files = await glob(pattern, {
77
- ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**', '**/build/**'],
85
+ ignore: EXCLUDE_PATTERNS_GLOB,
78
86
  absolute: true,
79
87
  });
80
88
  return files;
@@ -93,7 +101,7 @@ export class WorkspaceParser {
93
101
  * Injects the shared context so context is shared across all packages.
94
102
  */
95
103
  async createParserForPackage(pkg) {
96
- const tsConfigPath = pkg.tsConfigPath || path.join(pkg.path, 'tsconfig.json');
104
+ const tsConfigPath = pkg.tsConfigPath ?? path.join(pkg.path, 'tsconfig.json');
97
105
  let parser;
98
106
  if (this.projectType === 'auto') {
99
107
  // Auto-detect framework for this specific package
@@ -121,22 +129,22 @@ export class WorkspaceParser {
121
129
  * Parse a single package and return its results
122
130
  */
123
131
  async parsePackage(pkg) {
124
- console.log(`\nParsing package: ${pkg.name}`);
132
+ await debugLog(`Parsing package: ${pkg.name}`);
125
133
  const parser = await this.createParserForPackage(pkg);
126
134
  // Discover files for this package
127
135
  const files = await this.discoverPackageFiles(pkg);
128
136
  if (files.length === 0) {
129
- console.log(` ⚠️ No TypeScript files found in ${pkg.name}`);
137
+ await debugLog(`No TypeScript files found in ${pkg.name}`);
130
138
  return { nodes: [], edges: [] };
131
139
  }
132
- console.log(` 📄 ${files.length} files to parse`);
140
+ await debugLog(`${pkg.name}: ${files.length} files to parse`);
133
141
  // Parse all files in this package
134
142
  const result = await parser.parseChunk(files, true); // Skip edge resolution for now
135
143
  // Add package name to all nodes
136
144
  for (const node of result.nodes) {
137
145
  node.properties.packageName = pkg.name;
138
146
  }
139
- console.log(` ✅ ${result.nodes.length} nodes, ${result.edges.length} edges`);
147
+ await debugLog(`${pkg.name}: ${result.nodes.length} nodes, ${result.edges.length} edges`);
140
148
  return result;
141
149
  }
142
150
  /**
@@ -196,7 +204,22 @@ export class WorkspaceParser {
196
204
  // Continue with other packages
197
205
  }
198
206
  }
199
- return { nodes: allNodes, edges: allEdges };
207
+ // Only return nodes/edges that haven't been exported yet (prevents duplicate imports in streaming mode)
208
+ const newNodes = allNodes.filter((node) => {
209
+ if (!this.exportedNodeIds.has(node.id)) {
210
+ this.exportedNodeIds.add(node.id);
211
+ return true;
212
+ }
213
+ return false;
214
+ });
215
+ const newEdges = allEdges.filter((edge) => {
216
+ if (!this.exportedEdgeIds.has(edge.id)) {
217
+ this.exportedEdgeIds.add(edge.id);
218
+ return true;
219
+ }
220
+ return false;
221
+ });
222
+ return { nodes: newNodes, edges: newEdges };
200
223
  }
201
224
  /**
202
225
  * Find which package a file belongs to
@@ -225,8 +248,7 @@ export class WorkspaceParser {
225
248
  edges: result.edges.length,
226
249
  });
227
250
  }
228
- console.log(`\n🎉 Workspace parsing complete!`);
229
- console.log(` Total: ${allNodes.length} nodes, ${allEdges.length} edges`);
251
+ await debugLog(`Workspace parsing complete! Total: ${allNodes.length} nodes, ${allEdges.length} edges`);
230
252
  return {
231
253
  nodes: allNodes,
232
254
  edges: allEdges,
@@ -240,6 +262,8 @@ export class WorkspaceParser {
240
262
  clearParsedData() {
241
263
  this.parsedNodes.clear();
242
264
  this.parsedEdges.clear();
265
+ this.exportedNodeIds.clear();
266
+ this.exportedEdgeIds.clear();
243
267
  }
244
268
  /**
245
269
  * Add existing nodes for cross-package edge resolution
@@ -249,6 +273,25 @@ export class WorkspaceParser {
249
273
  this.parsedNodes.set(node.id, node);
250
274
  }
251
275
  }
276
+ /**
277
+ * Add nodes to accumulatedParsedNodes for edge enhancement.
278
+ * Converts Neo4jNode to LightweightParsedNode format.
279
+ */
280
+ addParsedNodesFromChunk(nodes) {
281
+ for (const node of nodes) {
282
+ this.parsedNodes.set(node.id, node);
283
+ // Also add to accumulatedParsedNodes for edge enhancement detection
284
+ this.accumulatedParsedNodes.set(node.id, {
285
+ id: node.id,
286
+ coreType: node.properties.coreType,
287
+ semanticType: node.properties.semanticType,
288
+ properties: {
289
+ name: node.properties.name,
290
+ context: node.properties.context,
291
+ },
292
+ });
293
+ }
294
+ }
252
295
  /**
253
296
  * Get current counts for progress reporting
254
297
  */
@@ -260,91 +303,126 @@ export class WorkspaceParser {
260
303
  };
261
304
  }
262
305
  /**
263
- * Resolve accumulated deferred edges against all parsed nodes
264
- * Call this after all chunks have been parsed
306
+ * Set whether to defer edge enhancements.
307
+ * WorkspaceParser always defers edge enhancements to applyEdgeEnhancementsManually(),
308
+ * so this is a no-op for interface compliance.
265
309
  */
266
- async resolveDeferredEdgesManually() {
267
- const resolvedEdges = [];
268
- const unresolvedImports = [];
269
- const unresolvedExtends = [];
270
- const unresolvedImplements = [];
271
- // Count by edge type for logging
272
- const importsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'IMPORTS').length;
273
- const extendsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'EXTENDS').length;
274
- const implementsCount = this.accumulatedDeferredEdges.filter((e) => e.edgeType === 'IMPLEMENTS').length;
275
- for (const deferred of this.accumulatedDeferredEdges) {
276
- // Find target node by name, type, and optionally file path from accumulated nodes
277
- const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
278
- if (targetNode) {
279
- // Find source node to get filePath
280
- const sourceNode = this.parsedNodes.get(deferred.sourceNodeId);
281
- const filePath = sourceNode?.properties.filePath ?? '';
282
- // Get relationship weight from core schema
283
- const coreEdgeType = deferred.edgeType;
284
- const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[coreEdgeType];
285
- const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
286
- // Generate a unique edge ID
287
- const edgeHash = crypto
288
- .createHash('md5')
289
- .update(`${deferred.sourceNodeId}-${deferred.edgeType}-${targetNode.id}`)
290
- .digest('hex')
291
- .substring(0, 12);
292
- const edge = {
293
- id: `${this.projectId}:${deferred.edgeType}:${edgeHash}`,
294
- type: deferred.edgeType,
295
- startNodeId: deferred.sourceNodeId,
296
- endNodeId: targetNode.id,
297
- properties: {
298
- coreType: coreEdgeType,
299
- projectId: this.projectId,
300
- source: 'ast',
301
- confidence: 1.0,
302
- relationshipWeight,
303
- filePath,
304
- createdAt: new Date().toISOString(),
305
- },
306
- };
307
- resolvedEdges.push(edge);
308
- }
309
- else {
310
- // Track unresolved by type
311
- if (deferred.edgeType === 'IMPORTS') {
312
- unresolvedImports.push(deferred.targetName);
310
+ setDeferEdgeEnhancements(_defer) {
311
+ // No-op: WorkspaceParser always handles edge enhancements at the end
312
+ }
313
+ /**
314
+ * Load framework schemas for a specific project type.
315
+ * Used by parallel parsing coordinator to load schemas before edge enhancement.
316
+ * In sequential parsing, schemas are accumulated from inner parsers instead.
317
+ */
318
+ loadFrameworkSchemasForType(projectType) {
319
+ // Load schemas based on project type (same logic as ParserFactory.selectFrameworkSchemas)
320
+ switch (projectType) {
321
+ case 'nestjs':
322
+ if (!this.frameworkSchemas.some((s) => s.name === NESTJS_FRAMEWORK_SCHEMA.name)) {
323
+ this.frameworkSchemas.push(NESTJS_FRAMEWORK_SCHEMA);
324
+ }
325
+ break;
326
+ case 'fairsquare':
327
+ if (!this.frameworkSchemas.some((s) => s.name === FAIRSQUARE_FRAMEWORK_SCHEMA.name)) {
328
+ this.frameworkSchemas.push(FAIRSQUARE_FRAMEWORK_SCHEMA);
313
329
  }
314
- else if (deferred.edgeType === 'EXTENDS') {
315
- unresolvedExtends.push(deferred.targetName);
330
+ break;
331
+ case 'both':
332
+ if (!this.frameworkSchemas.some((s) => s.name === FAIRSQUARE_FRAMEWORK_SCHEMA.name)) {
333
+ this.frameworkSchemas.push(FAIRSQUARE_FRAMEWORK_SCHEMA);
316
334
  }
317
- else if (deferred.edgeType === 'IMPLEMENTS') {
318
- unresolvedImplements.push(deferred.targetName);
335
+ if (!this.frameworkSchemas.some((s) => s.name === NESTJS_FRAMEWORK_SCHEMA.name)) {
336
+ this.frameworkSchemas.push(NESTJS_FRAMEWORK_SCHEMA);
319
337
  }
338
+ break;
339
+ // 'vanilla' and 'auto' - no framework schemas
340
+ }
341
+ debugLog('WorkspaceParser loaded framework schemas', { count: this.frameworkSchemas.length, projectType });
342
+ }
343
+ /**
344
+ * Get serialized shared context for parallel parsing.
345
+ * Converts Maps to arrays for structured clone compatibility.
346
+ */
347
+ getSerializedSharedContext() {
348
+ const serialized = [];
349
+ for (const [key, value] of this.sharedContext) {
350
+ if (value instanceof Map) {
351
+ serialized.push([key, Array.from(value.entries())]);
352
+ }
353
+ else {
354
+ serialized.push([key, value]);
320
355
  }
321
356
  }
322
- // Log resolution stats
323
- const importsResolved = resolvedEdges.filter((e) => e.type === 'IMPORTS').length;
324
- const extendsResolved = resolvedEdges.filter((e) => e.type === 'EXTENDS').length;
325
- const implementsResolved = resolvedEdges.filter((e) => e.type === 'IMPLEMENTS').length;
326
- debugLog('WorkspaceParser edge resolution', {
327
- totalDeferredEdges: this.accumulatedDeferredEdges.length,
328
- totalNodesAvailable: this.parsedNodes.size,
329
- imports: {
330
- queued: importsCount,
331
- resolved: importsResolved,
332
- unresolved: unresolvedImports.length,
333
- sample: unresolvedImports.slice(0, 10),
334
- },
335
- extends: {
336
- queued: extendsCount,
337
- resolved: extendsResolved,
338
- unresolved: unresolvedExtends.length,
339
- sample: unresolvedExtends.slice(0, 10),
340
- },
341
- implements: {
342
- queued: implementsCount,
343
- resolved: implementsResolved,
344
- unresolved: unresolvedImplements.length,
345
- sample: unresolvedImplements.slice(0, 10),
346
- },
347
- });
357
+ return serialized;
358
+ }
359
+ /**
360
+ * Merge serialized shared context from workers.
361
+ * Handles Map merging by combining entries.
362
+ */
363
+ mergeSerializedSharedContext(serialized) {
364
+ for (const [key, value] of serialized) {
365
+ if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
366
+ // It's a serialized Map - merge with existing
367
+ const existingMap = this.sharedContext.get(key);
368
+ const newMap = existingMap ?? new Map();
369
+ for (const [k, v] of value) {
370
+ newMap.set(k, v);
371
+ }
372
+ this.sharedContext.set(key, newMap);
373
+ }
374
+ else {
375
+ // Simple value - just set it
376
+ this.sharedContext.set(key, value);
377
+ }
378
+ }
379
+ }
380
+ /**
381
+ * Get deferred edges for cross-chunk resolution.
382
+ * Returns serializable format for worker thread transfer.
383
+ */
384
+ getDeferredEdges() {
385
+ return this.accumulatedDeferredEdges.map((e) => ({
386
+ edgeType: e.edgeType,
387
+ sourceNodeId: e.sourceNodeId,
388
+ targetName: e.targetName,
389
+ targetType: e.targetType,
390
+ targetFilePath: e.targetFilePath,
391
+ }));
392
+ }
393
+ /**
394
+ * Merge deferred edges from workers for resolution.
395
+ */
396
+ mergeDeferredEdges(edges) {
397
+ for (const e of edges) {
398
+ this.accumulatedDeferredEdges.push({
399
+ edgeType: e.edgeType,
400
+ sourceNodeId: e.sourceNodeId,
401
+ targetName: e.targetName,
402
+ targetType: e.targetType,
403
+ targetFilePath: e.targetFilePath,
404
+ });
405
+ }
406
+ }
407
+ /**
408
+ * Resolve accumulated deferred edges against all parsed nodes
409
+ * Call this after all chunks have been parsed
410
+ */
411
+ async resolveDeferredEdges() {
412
+ if (this.accumulatedDeferredEdges.length === 0) {
413
+ return [];
414
+ }
415
+ // Create or reuse resolver parser - delegates all resolution logic to TypeScriptParser
416
+ // Uses createResolver() which doesn't require ts-morph initialization
417
+ if (!this.resolverParser) {
418
+ this.resolverParser = TypeScriptParser.createResolver(this.projectId);
419
+ }
420
+ // Populate resolver with accumulated nodes (builds CALLS indexes automatically)
421
+ this.resolverParser.addParsedNodesFromChunk(Array.from(this.parsedNodes.values()));
422
+ // Transfer deferred edges to resolver
423
+ this.resolverParser.mergeDeferredEdges(this.accumulatedDeferredEdges);
424
+ // Delegate resolution to TypeScriptParser
425
+ const resolvedEdges = await this.resolverParser.resolveDeferredEdges();
348
426
  // Clear accumulated deferred edges after resolution
349
427
  this.accumulatedDeferredEdges = [];
350
428
  return resolvedEdges;
@@ -359,18 +437,18 @@ export class WorkspaceParser {
359
437
  */
360
438
  async applyEdgeEnhancementsManually() {
361
439
  if (this.accumulatedParsedNodes.size === 0) {
362
- console.log('WorkspaceParser: No accumulated nodes for edge enhancements');
440
+ await debugLog('WorkspaceParser: No accumulated nodes for edge enhancements');
363
441
  return [];
364
442
  }
365
443
  if (this.frameworkSchemas.length === 0) {
366
- console.log('WorkspaceParser: No framework schemas for edge enhancements');
444
+ await debugLog('WorkspaceParser: No framework schemas for edge enhancements');
367
445
  return [];
368
446
  }
369
- console.log(`WorkspaceParser: Applying edge enhancements on ${this.accumulatedParsedNodes.size} accumulated nodes across all packages...`);
447
+ await debugLog(`WorkspaceParser: Applying edge enhancements on ${this.accumulatedParsedNodes.size} accumulated nodes across all packages...`);
370
448
  // Pre-index nodes by semantic type for O(1) lookups
371
449
  const nodesBySemanticType = new Map();
372
450
  for (const [nodeId, node] of this.accumulatedParsedNodes) {
373
- const semanticType = node.semanticType || 'unknown';
451
+ const semanticType = node.semanticType ?? 'unknown';
374
452
  if (!nodesBySemanticType.has(semanticType)) {
375
453
  nodesBySemanticType.set(semanticType, new Map());
376
454
  }
@@ -380,7 +458,7 @@ export class WorkspaceParser {
380
458
  for (const [type, nodes] of nodesBySemanticType) {
381
459
  typeCounts[type] = nodes.size;
382
460
  }
383
- console.log(`Node distribution by semantic type:`, typeCounts);
461
+ await debugLog(`Node distribution by semantic type: ${JSON.stringify(typeCounts)}`);
384
462
  const newEdges = [];
385
463
  const edgeCountBefore = this.parsedEdges.size;
386
464
  // Apply edge enhancements from all framework schemas
@@ -391,7 +469,7 @@ export class WorkspaceParser {
391
469
  }
392
470
  }
393
471
  const newEdgeCount = this.parsedEdges.size - edgeCountBefore;
394
- console.log(`Created ${newEdgeCount} cross-package edges from edge enhancements`);
472
+ await debugLog(`Created ${newEdgeCount} cross-package edges from edge enhancements`);
395
473
  return newEdges;
396
474
  }
397
475
  /**
@@ -455,100 +533,4 @@ export class WorkspaceParser {
455
533
  properties,
456
534
  };
457
535
  }
458
- /**
459
- * Find a node by name and type from accumulated nodes
460
- * For SourceFiles, implements smart import resolution:
461
- * - Direct file path match
462
- * - Relative import resolution (./foo, ../bar)
463
- * - Scoped package imports (@workspace/ui, @ui/core)
464
- *
465
- * For ClassDeclaration/InterfaceDeclaration with filePath, uses precise matching.
466
- */
467
- findNodeByNameAndType(name, type, filePath) {
468
- const allNodes = [...this.parsedNodes.values()];
469
- // If we have a file path and it's not a SourceFile, use precise matching first
470
- if (filePath && type !== 'SourceFile') {
471
- for (const node of allNodes) {
472
- if (node.properties.coreType === type &&
473
- node.properties.name === name &&
474
- node.properties.filePath === filePath) {
475
- return node;
476
- }
477
- }
478
- // If precise match fails, fall through to name-only matching below
479
- }
480
- // For SOURCE_FILE with import specifier, try multiple matching strategies
481
- if (type === 'SourceFile') {
482
- // Strategy 1: Direct file path match
483
- for (const node of allNodes) {
484
- if (node.labels.includes(type) && node.properties.filePath === name) {
485
- return node;
486
- }
487
- }
488
- // Strategy 2: Resolve relative imports (./foo, ../bar)
489
- if (name.startsWith('.')) {
490
- // Normalize: remove leading ./ or ../
491
- const normalizedPath = name.replace(/^\.\.\//, '').replace(/^\.\//, '');
492
- // Try matching with common extensions
493
- const extensions = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'];
494
- for (const ext of extensions) {
495
- const searchPath = normalizedPath + ext;
496
- for (const node of allNodes) {
497
- if (node.labels.includes(type)) {
498
- // Match if filePath ends with the normalized path
499
- if (node.properties.filePath.endsWith(searchPath) ||
500
- node.properties.filePath.endsWith('/' + searchPath)) {
501
- return node;
502
- }
503
- }
504
- }
505
- }
506
- }
507
- // Strategy 3: Workspace package imports (@workspace/ui, @ui/core)
508
- if (name.startsWith('@')) {
509
- const parts = name.split('/');
510
- const packageName = parts.slice(0, 2).join('/'); // @scope/package
511
- const subPath = parts.slice(2).join('/'); // rest of path after package name
512
- // First, try to find an exact match with subpath
513
- if (subPath) {
514
- const extensions = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'];
515
- for (const ext of extensions) {
516
- const searchPath = subPath + ext;
517
- for (const node of allNodes) {
518
- if (node.labels.includes(type) && node.properties.packageName === packageName) {
519
- if (node.properties.filePath.endsWith(searchPath) ||
520
- node.properties.filePath.endsWith('/' + searchPath)) {
521
- return node;
522
- }
523
- }
524
- }
525
- }
526
- }
527
- // For bare package imports (@workspace/ui), look for index files
528
- if (!subPath) {
529
- for (const node of allNodes) {
530
- if (node.labels.includes(type) && node.properties.packageName === packageName) {
531
- const fileName = node.properties.name;
532
- if (fileName === 'index.ts' || fileName === 'index.tsx') {
533
- return node;
534
- }
535
- }
536
- }
537
- // If no index file, return any file from the package as a fallback
538
- for (const node of allNodes) {
539
- if (node.labels.includes(type) && node.properties.packageName === packageName) {
540
- return node;
541
- }
542
- }
543
- }
544
- }
545
- }
546
- // Default: exact name match (for non-SourceFile types like classes, interfaces)
547
- for (const node of allNodes) {
548
- if (node.properties.coreType === type && node.properties.name === name) {
549
- return node;
550
- }
551
- }
552
- return undefined;
553
- }
554
536
  }