arcvision 0.2.2 → 0.2.3

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.
@@ -1,7 +1,7 @@
1
1
  const { glob } = require('glob');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const parser = require('./parser');
4
+ const parser = require('./parser-enhanced'); // Use enhanced parser
5
5
  const pluginManager = require('../plugins/plugin-manager');
6
6
  const { loadTSConfig } = require('./tsconfig-utils');
7
7
  const { resolveImport } = require('./path-resolver');
@@ -12,10 +12,13 @@ const { buildContext } = require('../engine/context_builder');
12
12
  const { validateContext } = require('../engine/context_validator');
13
13
  const { sortContext } = require('../engine/context_sorter');
14
14
 
15
+ // Import semantic analyzer for cross-file analysis
16
+ const { analyzeSemantics } = require('./semantic-analyzer');
17
+
15
18
  async function scan(directory) {
16
19
  // Normalize helper
17
20
  const normalize = p => p.replace(/\\/g, '/');
18
-
21
+
19
22
  const options = {
20
23
  ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
21
24
  cwd: directory,
@@ -42,10 +45,10 @@ async function scan(directory) {
42
45
  try {
43
46
  const relativePath = path.relative(directory, file);
44
47
  const normalizedRelativePath = normalize(relativePath);
45
-
48
+
46
49
  // Handle different file types appropriately
47
50
  let metadata;
48
-
51
+
49
52
  if (file.endsWith('.json')) {
50
53
  // Handle JSON files separately
51
54
  try {
@@ -71,7 +74,7 @@ async function scan(directory) {
71
74
 
72
75
  // Process with plugins
73
76
  metadata = await pluginManager.processFile(file, metadata);
74
-
77
+
75
78
  // Count imports found in this file
76
79
  if (metadata.imports && Array.isArray(metadata.imports)) {
77
80
  totalImportsFound += metadata.imports.length;
@@ -90,21 +93,26 @@ async function scan(directory) {
90
93
  console.warn(`⚠️ Failed to process ${file} — file skipped, you should check manually (${e.message})`);
91
94
  }
92
95
  }
93
-
96
+
97
+ // NEW: Perform semantic analysis to build cross-file usage edges
98
+ console.log('Performing semantic analysis...');
99
+ const semanticResults = analyzeSemantics(architectureMap.nodes);
100
+ console.log(`Semantic analysis complete: ${semanticResults.stats.totalSymbols} symbols, ${semanticResults.stats.totalUsages} usage edges`);
101
+
94
102
  // Load tsconfig for path resolution
95
103
  const tsconfig = loadTSConfig(directory);
96
-
104
+
97
105
  // Create a mapping of all possible normalized paths that could match imports
98
106
  // This includes both the direct file paths and possible variations
99
107
  const allPossiblePaths = new Set();
100
108
  const pathToNodeIdMap = new Map(); // This maps normalized paths to node IDs
101
-
109
+
102
110
  architectureMap.nodes.forEach(node => {
103
111
  const normalizedPath = normalize(node.id);
104
112
  allPossiblePaths.add(normalizedPath);
105
113
  pathToNodeIdMap.set(normalizedPath, node.id);
106
114
  });
107
-
115
+
108
116
  // Process imports to create edges
109
117
  let unresolvedImports = 0;
110
118
  let resolvedImports = 0;
@@ -113,7 +121,7 @@ async function scan(directory) {
113
121
  node.metadata.imports.forEach(imp => {
114
122
  if (imp.source && typeof imp.source === 'string') {
115
123
  let targetFound = false;
116
-
124
+
117
125
  try {
118
126
  // First, check if imp.source directly matches any node path (exact match)
119
127
  if (allPossiblePaths.has(imp.source)) {
@@ -125,7 +133,7 @@ async function scan(directory) {
125
133
  resolvedImports++;
126
134
  targetFound = true;
127
135
  }
128
-
136
+
129
137
  if (!targetFound) {
130
138
  // Try to resolve the import path using the resolver
131
139
  const resolvedPath = resolveImport(
@@ -134,12 +142,12 @@ async function scan(directory) {
134
142
  directory,
135
143
  tsconfig
136
144
  );
137
-
145
+
138
146
  if (resolvedPath) {
139
147
  // Convert resolved absolute path back to relative path
140
148
  const relativeResolvedPath = path.relative(directory, resolvedPath);
141
149
  const normalizedResolvedPath = normalize(relativeResolvedPath);
142
-
150
+
143
151
  // Check if the resolved file exists in our scanned files
144
152
  if (allPossiblePaths.has(normalizedResolvedPath)) {
145
153
  architectureMap.edges.push({
@@ -152,7 +160,7 @@ async function scan(directory) {
152
160
  }
153
161
  }
154
162
  }
155
-
163
+
156
164
  if (!targetFound) {
157
165
  // As a fallback, check if the import path could match by simple relative path calculation
158
166
  // For example, if node is 'src/core/scanner.js' and import is './parser.js',
@@ -162,7 +170,7 @@ async function scan(directory) {
162
170
  const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
163
171
  const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
164
172
  const calculatedNormalizedPath = normalize(calculatedRelativePath);
165
-
173
+
166
174
  if (allPossiblePaths.has(calculatedNormalizedPath)) {
167
175
  architectureMap.edges.push({
168
176
  source: normalize(node.id),
@@ -177,27 +185,27 @@ async function scan(directory) {
177
185
  // If path calculation fails, continue to unresolved
178
186
  }
179
187
  }
180
-
188
+
181
189
  if (!targetFound) {
182
190
  // Another fallback: if import starts with ./ or ../, try to find a match
183
191
  // by appending common extensions to the import path
184
192
  if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
185
193
  // Try various extensions that might match
186
194
  const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
187
-
195
+
188
196
  for (const ext of extensions) {
189
197
  let testPath = imp.source;
190
198
  if (!imp.source.endsWith(ext)) {
191
199
  testPath = imp.source + ext;
192
200
  }
193
-
201
+
194
202
  // Calculate the relative path from the project directory
195
203
  try {
196
204
  const baseDir = path.dirname(path.join(directory, node.id));
197
205
  const calculatedAbsolutePath = path.resolve(baseDir, testPath);
198
206
  const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
199
207
  const calculatedNormalizedPath = normalize(calculatedRelativePath);
200
-
208
+
201
209
  if (allPossiblePaths.has(calculatedNormalizedPath)) {
202
210
  architectureMap.edges.push({
203
211
  source: normalize(node.id),
@@ -215,7 +223,7 @@ async function scan(directory) {
215
223
  }
216
224
  }
217
225
  }
218
-
226
+
219
227
  if (!targetFound) {
220
228
  // Additional fallback: Check for index files when importing a directory
221
229
  if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
@@ -223,14 +231,14 @@ async function scan(directory) {
223
231
  const baseDir = path.dirname(path.join(directory, node.id));
224
232
  const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
225
233
  const directoryPath = calculatedAbsolutePath;
226
-
234
+
227
235
  // Check for index files in the directory
228
236
  const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
229
237
  for (const indexFile of indexFiles) {
230
238
  const indexPath = path.join(directoryPath, indexFile);
231
239
  const indexRelativePath = path.relative(directory, indexPath);
232
240
  const indexNormalizedPath = normalize(indexRelativePath);
233
-
241
+
234
242
  if (allPossiblePaths.has(indexNormalizedPath)) {
235
243
  architectureMap.edges.push({
236
244
  source: normalize(node.id),
@@ -248,7 +256,7 @@ async function scan(directory) {
248
256
  }
249
257
  }
250
258
  }
251
-
259
+
252
260
  if (!targetFound) {
253
261
  // Additional fallback: Check for absolute imports that might map to common directories
254
262
  // For example, if tsconfig has paths like "@/*": ["src/*"], an import like "@/utils/helper"
@@ -256,13 +264,13 @@ async function scan(directory) {
256
264
  if (imp.source.startsWith('@') || imp.source.startsWith('/') || !imp.source.startsWith('.')) {
257
265
  // Try to match against common source directories
258
266
  const commonSourceDirs = ['src', 'app', 'lib', 'components', 'utils', 'services', 'assets'];
259
-
267
+
260
268
  for (const srcDir of commonSourceDirs) {
261
269
  // Try appending to common source directories
262
270
  const potentialPath = path.join(srcDir, imp.source);
263
271
  const potentialRelativePath = path.relative(directory, path.resolve(directory, potentialPath));
264
272
  const potentialNormalizedPath = normalize(potentialRelativePath);
265
-
273
+
266
274
  if (allPossiblePaths.has(potentialNormalizedPath)) {
267
275
  architectureMap.edges.push({
268
276
  source: normalize(node.id),
@@ -273,11 +281,11 @@ async function scan(directory) {
273
281
  targetFound = true;
274
282
  break; // Found a match, exit the loop
275
283
  }
276
-
284
+
277
285
  // Also try with extensions
278
286
  const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
279
- 'index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
280
-
287
+ 'index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
288
+
281
289
  for (const ext of extensions) {
282
290
  let testPath;
283
291
  if (ext.startsWith('index')) {
@@ -285,10 +293,10 @@ async function scan(directory) {
285
293
  } else {
286
294
  testPath = path.join(srcDir, imp.source + ext);
287
295
  }
288
-
296
+
289
297
  const testRelativePath = path.relative(directory, path.resolve(directory, testPath));
290
298
  const testNormalizedPath = normalize(testRelativePath);
291
-
299
+
292
300
  if (allPossiblePaths.has(testNormalizedPath)) {
293
301
  architectureMap.edges.push({
294
302
  source: normalize(node.id),
@@ -300,12 +308,12 @@ async function scan(directory) {
300
308
  break; // Found a match, exit the loops
301
309
  }
302
310
  }
303
-
311
+
304
312
  if (targetFound) break;
305
313
  }
306
314
  }
307
315
  }
308
-
316
+
309
317
  if (!targetFound) {
310
318
  // Additional fallback: Try to handle barrel files (index.js that exports from other files)
311
319
  // If import is '@/components/Header' but only '@/components/index.js' exists
@@ -317,17 +325,17 @@ async function scan(directory) {
317
325
  // Remove the last part and add index
318
326
  const dirParts = parts.slice(0, -1);
319
327
  const fileName = parts[parts.length - 1];
320
-
328
+
321
329
  // Look for patterns like: components/index.js exporting { Header }
322
330
  // or components/index.js containing export * from './Header'
323
331
  const dirPath = dirParts.join('/') + '/index';
324
332
  const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
325
-
333
+
326
334
  for (const indexFile of indexFiles) {
327
335
  const indexPath = path.join(dirParts.join('/'), indexFile);
328
336
  const indexRelativePath = path.relative(directory, path.resolve(directory, indexPath));
329
337
  const indexNormalizedPath = normalize(indexRelativePath);
330
-
338
+
331
339
  if (allPossiblePaths.has(indexNormalizedPath)) {
332
340
  architectureMap.edges.push({
333
341
  source: normalize(node.id),
@@ -346,7 +354,7 @@ async function scan(directory) {
346
354
  }
347
355
  }
348
356
  }
349
-
357
+
350
358
  if (!targetFound) {
351
359
  // Additional fallback: Check for exact path matches with different capitalization
352
360
  // This handles cases where import uses different casing than actual file
@@ -369,7 +377,7 @@ async function scan(directory) {
369
377
  // Even if there's an error processing this import, increment unresolved counter
370
378
  unresolvedImports++;
371
379
  }
372
-
380
+
373
381
  if (!targetFound) {
374
382
  // Track imports that couldn't be matched to existing nodes
375
383
  unresolvedImports++;
@@ -380,35 +388,53 @@ async function scan(directory) {
380
388
  });
381
389
  console.log('RESOLVED IMPORTS:', resolvedImports);
382
390
  console.log('UNRESOLVED IMPORTS:', unresolvedImports);
383
- console.log('EDGES CREATED:', architectureMap.edges.length);
391
+ console.log('IMPORT EDGES CREATED:', architectureMap.edges.length);
392
+
393
+ // NEW: Add semantic usage edges
394
+ if (semanticResults && semanticResults.usageEdges) {
395
+ semanticResults.usageEdges.forEach(usageEdge => {
396
+ // Only add if both source and target exist in our nodes
397
+ const sourceExists = architectureMap.nodes.some(n => n.id === usageEdge.source);
398
+ const targetExists = architectureMap.nodes.some(n => n.id === usageEdge.target);
399
+
400
+ if (sourceExists && targetExists) {
401
+ architectureMap.edges.push({
402
+ source: normalize(usageEdge.source),
403
+ target: normalize(usageEdge.target),
404
+ type: usageEdge.type // 'function_call', 'constructor_call', 'component_usage'
405
+ });
406
+ }
407
+ });
408
+ }
409
+ console.log('TOTAL EDGES (with usage):', architectureMap.edges.length);
384
410
 
385
411
  // Calculate blast radius for each file
386
412
  const reverseGraph = buildReverseDependencyGraph(architectureMap);
387
413
  const blastRadiusMap = computeBlastRadius(reverseGraph);
388
-
414
+
389
415
  // Add blast_radius to each node
390
416
  architectureMap.nodes.forEach(node => {
391
417
  node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
392
418
  });
393
-
419
+
394
420
  console.log('BEFORE DEDUPLICATION EDGES:', architectureMap.edges.length);
395
-
421
+
396
422
  // Process edges to ensure they match existing nodes and remove duplicates
397
423
  const validEdges = [];
398
424
  const edgeSet = new Set(); // To track unique edges
399
-
425
+
400
426
  architectureMap.edges.forEach(edge => {
401
427
  // Normalize both source and target paths for consistent matching
402
428
  const normalizedSource = normalize(edge.source);
403
429
  const normalizedTarget = normalize(edge.target);
404
-
430
+
405
431
  // Create a unique key for the edge to avoid duplicates
406
432
  const edgeKey = `${normalizedSource}→${normalizedTarget}`;
407
-
433
+
408
434
  // Check if both source and target nodes exist in our architecture map
409
435
  const sourceNodeExists = architectureMap.nodes.some(node => node.id === normalizedSource);
410
436
  const targetNodeExists = architectureMap.nodes.some(node => node.id === normalizedTarget);
411
-
437
+
412
438
  if (sourceNodeExists && targetNodeExists && !edgeSet.has(edgeKey)) {
413
439
  edgeSet.add(edgeKey);
414
440
  validEdges.push({
@@ -418,28 +444,28 @@ async function scan(directory) {
418
444
  });
419
445
  }
420
446
  });
421
-
447
+
422
448
  console.log('AFTER DEDUPLICATION EDGES:', validEdges.length);
423
-
449
+
424
450
  // Replace the edges with only valid ones
425
451
  architectureMap.edges = validEdges;
426
-
452
+
427
453
  // Add top blast radius files to the architecture map
428
454
  const totalFiles = architectureMap.nodes.length;
429
455
  const { computeBlastRadiusWithPercentage, analyzeCriticality } = require('./blastRadius');
430
456
  const allFilesWithPercentage = computeBlastRadiusWithPercentage(blastRadiusMap, totalFiles);
431
-
457
+
432
458
  // Analyze criticality of files
433
459
  const criticalityAnalysis = analyzeCriticality(blastRadiusMap, reverseGraph, architectureMap.nodes);
434
-
460
+
435
461
  // Sort by blast radius descending and take top 3
436
462
  const topFiles = allFilesWithPercentage.slice(0, 3);
437
-
463
+
438
464
  architectureMap.contextSurface = {
439
465
  topBlastRadiusFiles: topFiles,
440
466
  criticalityAnalysis: criticalityAnalysis.slice(0, 5) // Top 5 most critical files
441
467
  };
442
-
468
+
443
469
  // Build the new context object that conforms to the schema
444
470
  let context = buildContext(architectureMap.nodes, architectureMap.edges, {
445
471
  directory: directory,
@@ -447,9 +473,9 @@ async function scan(directory) {
447
473
  language: 'javascript',
448
474
  contextSurface: architectureMap.contextSurface
449
475
  });
450
-
476
+
451
477
  console.log('Validating structural context...');
452
-
478
+
453
479
  // Validate the context against the schema
454
480
  let validation = validateContext(context);
455
481
 
@@ -458,7 +484,7 @@ async function scan(directory) {
458
484
  validation.errors.forEach(e => console.error(' -', e));
459
485
 
460
486
  console.log('Re-running generation deterministically...');
461
-
487
+
462
488
  // Rebuild context deterministically
463
489
  context = buildContext(architectureMap.nodes, architectureMap.edges, {
464
490
  directory: directory,
@@ -476,12 +502,12 @@ async function scan(directory) {
476
502
  process.exit(1);
477
503
  }
478
504
  }
479
-
505
+
480
506
  // Sort the context deterministically
481
507
  const sortedContext = sortContext(context);
482
-
508
+
483
509
  console.log('STRUCTURAL CONTEXT VALIDATED — READY FOR UPLOAD');
484
-
510
+
485
511
  return sortedContext;
486
512
 
487
513
  } catch (err) {
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Semantic Analyzer
3
+ *
4
+ * Builds cross-file symbol resolution and tracks actual usage of imported symbols.
5
+ * This enables detection of which files actually USE imported functions/classes,
6
+ * not just which files import them.
7
+ */
8
+
9
+ /**
10
+ * Build a global symbol table from all file exports
11
+ * @param {Array} nodes - All scanned file nodes with metadata
12
+ * @returns {Map} Symbol table mapping symbol names to their source files
13
+ */
14
+ function buildSymbolTable(nodes) {
15
+ const symbolTable = new Map();
16
+
17
+ nodes.forEach(node => {
18
+ const filePath = node.id;
19
+ const metadata = node.metadata;
20
+
21
+ if (!metadata) return;
22
+
23
+ // Register all exports from this file
24
+ if (metadata.exports && Array.isArray(metadata.exports)) {
25
+ metadata.exports.forEach(exp => {
26
+ const symbolName = exp.name;
27
+ if (!symbolTable.has(symbolName)) {
28
+ symbolTable.set(symbolName, []);
29
+ }
30
+ symbolTable.get(symbolName).push({
31
+ file: filePath,
32
+ type: exp.type,
33
+ loc: exp.loc
34
+ });
35
+ });
36
+ }
37
+
38
+ // Register all functions
39
+ if (metadata.functions && Array.isArray(metadata.functions)) {
40
+ metadata.functions.forEach(func => {
41
+ const symbolName = func.name;
42
+ if (!symbolTable.has(symbolName)) {
43
+ symbolTable.set(symbolName, []);
44
+ }
45
+ symbolTable.get(symbolName).push({
46
+ file: filePath,
47
+ type: 'function',
48
+ loc: func.loc
49
+ });
50
+ });
51
+ }
52
+
53
+ // Register all classes
54
+ if (metadata.classes && Array.isArray(metadata.classes)) {
55
+ metadata.classes.forEach(cls => {
56
+ const symbolName = cls.name;
57
+ if (!symbolTable.has(symbolName)) {
58
+ symbolTable.set(symbolName, []);
59
+ }
60
+ symbolTable.get(symbolName).push({
61
+ file: filePath,
62
+ type: 'class',
63
+ loc: cls.loc
64
+ });
65
+ });
66
+ }
67
+ });
68
+
69
+ return symbolTable;
70
+ }
71
+
72
+ /**
73
+ * Resolve symbol usage to their definitions
74
+ * @param {Object} node - File node with metadata
75
+ * @param {Map} symbolTable - Global symbol table
76
+ * @returns {Array} Array of resolved symbol usages
77
+ */
78
+ function resolveSymbolUsage(node, symbolTable) {
79
+ const resolvedUsages = [];
80
+ const metadata = node.metadata;
81
+
82
+ if (!metadata) return resolvedUsages;
83
+
84
+ // Track which symbols this file imports
85
+ const importedSymbols = new Map();
86
+ if (metadata.imports && Array.isArray(metadata.imports)) {
87
+ metadata.imports.forEach(imp => {
88
+ if (imp.specifiers && Array.isArray(imp.specifiers)) {
89
+ imp.specifiers.forEach(spec => {
90
+ const localName = spec.local || spec.imported || spec;
91
+ const importedName = spec.imported || spec.local || spec;
92
+ importedSymbols.set(localName, {
93
+ source: imp.source,
94
+ originalName: importedName
95
+ });
96
+ });
97
+ }
98
+ });
99
+ }
100
+
101
+ // Check function calls against imported symbols
102
+ if (metadata.functionCalls && Array.isArray(metadata.functionCalls)) {
103
+ metadata.functionCalls.forEach(call => {
104
+ const callName = call.name;
105
+ if (importedSymbols.has(callName)) {
106
+ const importInfo = importedSymbols.get(callName);
107
+ resolvedUsages.push({
108
+ type: 'function_call',
109
+ symbol: callName,
110
+ source: importInfo.source,
111
+ loc: call.loc
112
+ });
113
+ }
114
+ });
115
+ }
116
+
117
+ // Check constructor calls against imported symbols
118
+ if (metadata.constructorCalls && Array.isArray(metadata.constructorCalls)) {
119
+ metadata.constructorCalls.forEach(call => {
120
+ const className = call.className;
121
+ if (importedSymbols.has(className)) {
122
+ const importInfo = importedSymbols.get(className);
123
+ resolvedUsages.push({
124
+ type: 'constructor_call',
125
+ symbol: className,
126
+ source: importInfo.source,
127
+ loc: call.loc
128
+ });
129
+ }
130
+ });
131
+ }
132
+
133
+ // Check component usage (React/JSX)
134
+ if (metadata.componentUsage && Array.isArray(metadata.componentUsage)) {
135
+ metadata.componentUsage.forEach(comp => {
136
+ const componentName = comp.component;
137
+ if (importedSymbols.has(componentName)) {
138
+ const importInfo = importedSymbols.get(componentName);
139
+ resolvedUsages.push({
140
+ type: 'component_usage',
141
+ symbol: componentName,
142
+ source: importInfo.source,
143
+ loc: comp.loc
144
+ });
145
+ }
146
+ });
147
+ }
148
+
149
+ return resolvedUsages;
150
+ }
151
+
152
+ /**
153
+ * Track cross-file references and build usage edges
154
+ * @param {Array} nodes - All scanned file nodes
155
+ * @param {Map} symbolTable - Global symbol table
156
+ * @returns {Array} Array of usage edges
157
+ */
158
+ function trackCrossFileReferences(nodes, symbolTable) {
159
+ const usageEdges = [];
160
+
161
+ nodes.forEach(node => {
162
+ const filePath = node.id;
163
+ const resolvedUsages = resolveSymbolUsage(node, symbolTable);
164
+
165
+ resolvedUsages.forEach(usage => {
166
+ // Create an edge from this file to the source of the symbol
167
+ usageEdges.push({
168
+ source: filePath,
169
+ target: usage.source,
170
+ type: usage.type,
171
+ symbol: usage.symbol,
172
+ loc: usage.loc
173
+ });
174
+ });
175
+ });
176
+
177
+ return usageEdges;
178
+ }
179
+
180
+ /**
181
+ * Analyze semantic relationships in the codebase
182
+ * @param {Array} nodes - All scanned file nodes
183
+ * @returns {Object} Semantic analysis results
184
+ */
185
+ function analyzeSemantics(nodes) {
186
+ const symbolTable = buildSymbolTable(nodes);
187
+ const usageEdges = trackCrossFileReferences(nodes, symbolTable);
188
+
189
+ return {
190
+ symbolTable,
191
+ usageEdges,
192
+ stats: {
193
+ totalSymbols: symbolTable.size,
194
+ totalUsages: usageEdges.length
195
+ }
196
+ };
197
+ }
198
+
199
+ module.exports = {
200
+ buildSymbolTable,
201
+ resolveSymbolUsage,
202
+ trackCrossFileReferences,
203
+ analyzeSemantics
204
+ };