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
@@ -1,36 +1,18 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import crypto from 'crypto';
3
2
  import fs from 'fs/promises';
4
3
  import path from 'node:path';
5
4
  import { glob } from 'glob';
6
5
  import { minimatch } from 'minimatch';
7
6
  import { Project, Node } from 'ts-morph';
8
- import { createFrameworkEdgeData } from '../utils/edge-factory.js';
9
- /**
10
- * Generate a deterministic node ID based on stable properties.
11
- * This ensures the same node gets the same ID across reparses.
12
- *
13
- * Identity is based on: projectId + coreType + filePath + name (+ parentId for nested nodes)
14
- * This is stable because when it matters (one side of edge not reparsed),
15
- * names are guaranteed unchanged (or imports would break, triggering reparse).
16
- *
17
- * Including projectId ensures nodes from different projects have unique IDs
18
- * even if they have identical file paths and names.
19
- */
20
- const generateDeterministicId = (projectId, coreType, filePath, name, parentId) => {
21
- const parts = parentId ? [projectId, coreType, filePath, parentId, name] : [projectId, coreType, filePath, name];
22
- const identity = parts.join('::');
23
- const hash = crypto.createHash('sha256').update(identity).digest('hex').substring(0, 16);
24
- return `${projectId}:${coreType}:${hash}`;
25
- };
26
- import { debugLog, hashFile } from '../utils/file-utils.js';
7
+ import { EXCLUDE_PATTERNS_GLOB, BUILT_IN_FUNCTIONS, BUILT_IN_METHODS, BUILT_IN_CLASSES } from '../../constants.js';
27
8
  import { NESTJS_FRAMEWORK_SCHEMA } from '../config/nestjs-framework-schema.js';
28
9
  import { CoreNodeType, CORE_TYPESCRIPT_SCHEMA, DEFAULT_PARSE_OPTIONS, CoreEdgeType, } from '../config/schema.js';
10
+ import { normalizeCode } from '../utils/code-normalizer.js';
11
+ import { debugLog, hashFile, matchesPattern, cleanTypeName } from '../utils/file-utils.js';
12
+ import { createFrameworkEdgeData, createCoreEdge as createCoreEdgeFactory, createCallsEdge as createCallsEdgeFactory, toNeo4jNode, toNeo4jEdge, toParsedEdge, generateDeterministicId, } from '../utils/graph-factory.js';
29
13
  import { resolveProjectId } from '../utils/project-id.js';
30
14
  export class TypeScriptParser {
31
- workspacePath;
32
- tsConfigPath;
33
- project;
15
+ project; // initialized in constructor, undefined in resolver-only mode
34
16
  coreSchema;
35
17
  parseConfig;
36
18
  frameworkSchemas;
@@ -43,17 +25,50 @@ export class TypeScriptParser {
43
25
  lazyLoad; // Whether to use lazy file loading for large projects
44
26
  discoveredFiles = null; // Cached file discovery results
45
27
  deferEdgeEnhancements = false; // When true, skip edge enhancements (parent will handle)
28
+ // Lookup indexes for efficient CALLS edge resolution
29
+ methodsByClass = new Map(); // className -> methodName -> node
30
+ functionsByName = new Map(); // functionName -> node
31
+ constructorsByClass = new Map(); // className -> constructor node
32
+ // Track already exported items to avoid returning duplicates in streaming mode
33
+ exportedNodeIds = new Set();
34
+ exportedEdgeIds = new Set();
35
+ workspacePath;
36
+ /**
37
+ * Create a resolver-only instance for edge resolution without parsing.
38
+ * This mode doesn't initialize ts-morph and can only resolve edges from externally-added nodes.
39
+ */
40
+ static createResolver(projectId) {
41
+ const instance = Object.create(TypeScriptParser.prototype);
42
+ // project is intentionally not initialized - resolver mode doesn't need ts-morph
43
+ instance.coreSchema = CORE_TYPESCRIPT_SCHEMA;
44
+ instance.parseConfig = DEFAULT_PARSE_OPTIONS;
45
+ instance.frameworkSchemas = [];
46
+ instance.parsedNodes = new Map();
47
+ instance.parsedEdges = new Map();
48
+ instance.existingNodes = new Map();
49
+ instance.deferredEdges = [];
50
+ instance.sharedContext = new Map();
51
+ instance.projectId = projectId;
52
+ instance.lazyLoad = true;
53
+ instance.discoveredFiles = null;
54
+ instance.deferEdgeEnhancements = false;
55
+ instance.methodsByClass = new Map();
56
+ instance.functionsByName = new Map();
57
+ instance.constructorsByClass = new Map();
58
+ instance.exportedNodeIds = new Set();
59
+ instance.exportedEdgeIds = new Set();
60
+ instance.workspacePath = '';
61
+ return instance;
62
+ }
46
63
  constructor(workspacePath, tsConfigPath = 'tsconfig.json', coreSchema = CORE_TYPESCRIPT_SCHEMA, frameworkSchemas = [NESTJS_FRAMEWORK_SCHEMA], parseConfig = DEFAULT_PARSE_OPTIONS, projectId, // Optional - derived from workspacePath if not provided
47
64
  lazyLoad = false) {
48
65
  this.workspacePath = workspacePath;
49
- this.tsConfigPath = tsConfigPath;
50
66
  this.coreSchema = coreSchema;
51
67
  this.frameworkSchemas = frameworkSchemas;
52
68
  this.parseConfig = parseConfig;
53
69
  this.projectId = resolveProjectId(workspacePath, projectId);
54
70
  this.lazyLoad = lazyLoad;
55
- console.log(`🆔 Project ID: ${this.projectId}`);
56
- console.log(`📂 Lazy loading: ${lazyLoad ? 'enabled' : 'disabled'}`);
71
+ debugLog('Parser initialized', { projectId: this.projectId, lazyLoad });
57
72
  if (lazyLoad) {
58
73
  // Lazy mode: create Project without loading any files
59
74
  // Files will be added just-in-time during parseChunk()
@@ -97,6 +112,35 @@ export class TypeScriptParser {
97
112
  getProjectId() {
98
113
  return this.projectId;
99
114
  }
115
+ /**
116
+ * Get all parsed nodes (for cross-parser edge resolution).
117
+ * Returns the internal Map of ParsedNodes.
118
+ */
119
+ getParsedNodes() {
120
+ return this.parsedNodes;
121
+ }
122
+ /**
123
+ * Get the framework schemas used by this parser.
124
+ * Useful for WorkspaceParser to apply cross-package edge enhancements.
125
+ */
126
+ getFrameworkSchemas() {
127
+ return this.frameworkSchemas;
128
+ }
129
+ /**
130
+ * Get the shared context from this parser.
131
+ * Useful for aggregating context across multiple parsers.
132
+ */
133
+ getSharedContext() {
134
+ return this.sharedContext;
135
+ }
136
+ /**
137
+ * Set the shared context for this parser.
138
+ * Use this to share context across multiple parsers (e.g., in WorkspaceParser).
139
+ * @param context The shared context map to use
140
+ */
141
+ setSharedContext(context) {
142
+ this.sharedContext = context;
143
+ }
100
144
  /**
101
145
  * Set existing nodes from Neo4j for edge target matching during incremental parsing.
102
146
  * These nodes will be available as targets for edge detection but won't be exported.
@@ -122,63 +166,204 @@ export class TypeScriptParser {
122
166
  };
123
167
  this.existingNodes.set(node.id, parsedNode);
124
168
  }
125
- console.log(`📦 Loaded ${nodes.length} existing nodes for edge detection`);
169
+ debugLog('Loaded existing nodes for edge detection', { count: nodes.length });
170
+ }
171
+ /**
172
+ * Defer edge enhancements to a parent parser (e.g., WorkspaceParser).
173
+ * When true, parseChunk() will skip applyEdgeEnhancements().
174
+ * The parent is responsible for calling applyEdgeEnhancementsManually() at the end.
175
+ */
176
+ setDeferEdgeEnhancements(defer) {
177
+ this.deferEdgeEnhancements = defer;
178
+ }
179
+ /**
180
+ * Load framework schemas for a specific project type.
181
+ * No-op for TypeScriptParser since schemas are loaded in constructor.
182
+ */
183
+ loadFrameworkSchemasForType(_projectType) {
184
+ // TypeScriptParser already has schemas loaded via constructor/ParserFactory
126
185
  }
127
186
  async parseWorkspace(filesToParse) {
128
187
  let sourceFiles;
188
+ // Determine which files to parse
189
+ let filePaths;
129
190
  if (filesToParse && filesToParse.length > 0) {
130
- // In lazy mode, files may not be loaded yet - add them if needed
131
- sourceFiles = filesToParse
132
- .map((filePath) => {
133
- const existing = this.project.getSourceFile(filePath);
134
- if (existing)
135
- return existing;
136
- // Add file to project if not already loaded (lazy mode)
137
- try {
138
- return this.project.addSourceFileAtPath(filePath);
139
- }
140
- catch {
141
- return undefined;
142
- }
143
- })
144
- .filter((sf) => sf !== undefined);
191
+ filePaths = filesToParse;
192
+ }
193
+ else if (this.lazyLoad) {
194
+ // In lazy mode, use glob-based discovery (consistent with detectChangedFiles)
195
+ filePaths = await this.discoverSourceFiles();
145
196
  }
146
197
  else {
198
+ // Eager mode - files already loaded from tsconfig
147
199
  sourceFiles = this.project.getSourceFiles();
200
+ for (const sourceFile of sourceFiles) {
201
+ if (this.shouldSkipFile(sourceFile))
202
+ continue;
203
+ await this.parseCoreTypeScript(sourceFile);
204
+ }
205
+ return this.finishParsing();
148
206
  }
207
+ // Load files into project (for lazy mode or explicit file list)
208
+ sourceFiles = filePaths
209
+ .map((filePath) => {
210
+ const existing = this.project.getSourceFile(filePath);
211
+ if (existing)
212
+ return existing;
213
+ try {
214
+ return this.project.addSourceFileAtPath(filePath);
215
+ }
216
+ catch {
217
+ return undefined;
218
+ }
219
+ })
220
+ .filter((sf) => sf !== undefined);
149
221
  for (const sourceFile of sourceFiles) {
150
222
  if (this.shouldSkipFile(sourceFile))
151
223
  continue;
152
- await this.parseCoreTypeScriptV2(sourceFile);
153
- }
154
- await this.resolveDeferredEdges();
155
- await this.applyContextExtractors();
156
- if (this.frameworkSchemas.length > 0) {
157
- await this.applyFrameworkEnhancements();
224
+ await this.parseCoreTypeScript(sourceFile);
158
225
  }
159
- await this.applyEdgeEnhancements();
160
- const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
161
- const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
162
- return { nodes: neo4jNodes, edges: neo4jEdges };
226
+ return this.finishParsing();
163
227
  }
164
228
  /**
165
- * Check if variable declarations should be parsed for this file
166
- * based on framework schema configurations
229
+ * Parse a chunk of files without resolving deferred edges.
230
+ * Use this for streaming parsing where edges are resolved after all chunks.
231
+ * In lazy mode, files are added to the project just-in-time and removed after parsing.
232
+ * @param filePaths Specific file paths to parse
233
+ * @param skipEdgeResolution If true, deferred edges are not resolved (default: false)
167
234
  */
168
- shouldParseVariables(filePath) {
169
- for (const schema of this.frameworkSchemas) {
170
- const parsePatterns = schema.metadata.parseVariablesFrom;
171
- if (parsePatterns) {
172
- for (const pattern of parsePatterns) {
173
- if (minimatch(filePath, pattern)) {
174
- return true;
235
+ async parseChunk(filePaths, skipEdgeResolution = false) {
236
+ // Declare sourceFiles outside try so it's available in finally
237
+ const sourceFiles = [];
238
+ try {
239
+ if (this.lazyLoad) {
240
+ // Lazy mode: add files to project just-in-time
241
+ for (const filePath of filePaths) {
242
+ try {
243
+ // Check if file already exists in project (shouldn't happen in lazy mode)
244
+ // Add the file to the project if not already present
245
+ const sourceFile = this.project.getSourceFile(filePath) ?? this.project.addSourceFileAtPath(filePath);
246
+ sourceFiles.push(sourceFile);
175
247
  }
248
+ catch (error) {
249
+ console.warn(`Failed to add source file ${filePath}:`, error);
250
+ }
251
+ }
252
+ }
253
+ else {
254
+ // Eager mode: files are already loaded
255
+ const loadedFiles = filePaths
256
+ .map((filePath) => this.project.getSourceFile(filePath))
257
+ .filter((sf) => sf !== undefined);
258
+ sourceFiles.push(...loadedFiles);
259
+ }
260
+ for (const sourceFile of sourceFiles) {
261
+ if (this.shouldSkipFile(sourceFile))
262
+ continue;
263
+ await this.parseCoreTypeScript(sourceFile);
264
+ }
265
+ // Only resolve edges if not skipping
266
+ if (!skipEdgeResolution) {
267
+ await this.resolveDeferredEdges();
268
+ }
269
+ await this.applyContextExtractors();
270
+ if (this.frameworkSchemas.length > 0) {
271
+ await this.applyFrameworkEnhancements();
272
+ }
273
+ // Apply edge enhancements unless deferred to parent (e.g., WorkspaceParser)
274
+ // When deferred, parent will call applyEdgeEnhancementsManually() at the end
275
+ // with all accumulated nodes for cross-package edge detection
276
+ if (!this.deferEdgeEnhancements) {
277
+ await this.applyEdgeEnhancements();
278
+ }
279
+ // Only return nodes/edges that haven't been exported yet (prevents duplicate imports in streaming mode)
280
+ const newNodes = [];
281
+ const newEdges = [];
282
+ for (const node of this.parsedNodes.values()) {
283
+ if (!this.exportedNodeIds.has(node.id)) {
284
+ newNodes.push(toNeo4jNode(node));
285
+ this.exportedNodeIds.add(node.id);
286
+ }
287
+ }
288
+ for (const edge of this.parsedEdges.values()) {
289
+ if (!this.exportedEdgeIds.has(edge.id)) {
290
+ newEdges.push(toNeo4jEdge(edge));
291
+ this.exportedEdgeIds.add(edge.id);
176
292
  }
177
293
  }
294
+ return { nodes: newNodes, edges: newEdges };
178
295
  }
179
- return false;
296
+ finally {
297
+ // Always clean up in lazy mode to prevent memory leaks
298
+ if (this.lazyLoad) {
299
+ for (const sourceFile of sourceFiles) {
300
+ try {
301
+ this.project.removeSourceFile(sourceFile);
302
+ }
303
+ catch {
304
+ // Ignore errors when removing files
305
+ }
306
+ }
307
+ }
308
+ }
309
+ }
310
+ /**
311
+ * Get list of source files in the project.
312
+ * In lazy mode, uses glob to discover files without loading them into memory.
313
+ * Useful for determining total work and creating chunks.
314
+ */
315
+ async discoverSourceFiles() {
316
+ if (this.discoveredFiles !== null) {
317
+ return this.discoveredFiles;
318
+ }
319
+ if (this.lazyLoad) {
320
+ // Use glob to find files without loading them into ts-morph
321
+ // Use EXCLUDE_PATTERNS_GLOB for consistency with detectChangedFiles
322
+ const pattern = path.join(this.workspacePath, '**/*.{ts,tsx}');
323
+ this.discoveredFiles = await glob(pattern, {
324
+ ignore: EXCLUDE_PATTERNS_GLOB,
325
+ absolute: true,
326
+ });
327
+ debugLog('Discovered TypeScript files (lazy mode)', { count: this.discoveredFiles.length });
328
+ return this.discoveredFiles;
329
+ }
330
+ else {
331
+ // Eager mode - files are already loaded
332
+ this.discoveredFiles = this.project
333
+ .getSourceFiles()
334
+ .filter((sf) => !this.shouldSkipFile(sf))
335
+ .map((sf) => sf.getFilePath());
336
+ return this.discoveredFiles;
337
+ }
338
+ }
339
+ /**
340
+ * @deprecated Use discoverSourceFiles() instead for async file discovery
341
+ */
342
+ getSourceFilePaths() {
343
+ if (this.lazyLoad) {
344
+ throw new Error('getSourceFilePaths() is not supported in lazy mode. Use discoverSourceFiles() instead.');
345
+ }
346
+ return this.project
347
+ .getSourceFiles()
348
+ .filter((sf) => !this.shouldSkipFile(sf))
349
+ .map((sf) => sf.getFilePath());
350
+ }
351
+ /**
352
+ * Complete parsing by resolving edges and applying enhancements.
353
+ * Called after all source files have been parsed.
354
+ */
355
+ async finishParsing() {
356
+ await this.resolveDeferredEdges();
357
+ await this.applyContextExtractors();
358
+ if (this.frameworkSchemas.length > 0) {
359
+ await this.applyFrameworkEnhancements();
360
+ }
361
+ await this.applyEdgeEnhancements();
362
+ const neo4jNodes = Array.from(this.parsedNodes.values()).map(toNeo4jNode);
363
+ const neo4jEdges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
364
+ return { nodes: neo4jNodes, edges: neo4jEdges };
180
365
  }
181
- async parseCoreTypeScriptV2(sourceFile) {
366
+ async parseCoreTypeScript(sourceFile) {
182
367
  const filePath = sourceFile.getFilePath();
183
368
  const stats = await fs.stat(filePath);
184
369
  const fileTrackingProperties = {
@@ -221,16 +406,17 @@ export class TypeScriptParser {
221
406
  });
222
407
  }
223
408
  }
224
- if (this.shouldParseVariables(sourceFile.getFilePath())) {
225
- for (const varStatement of sourceFile.getVariableStatements()) {
226
- for (const varDecl of varStatement.getDeclarations()) {
227
- if (this.shouldSkipChildNode(varDecl))
228
- continue;
229
- const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
230
- this.addNode(variableNode);
231
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
232
- this.addEdge(containsEdge);
233
- }
409
+ for (const varStatement of sourceFile.getVariableStatements()) {
410
+ const isExported = varStatement.isExported();
411
+ if (!isExported && !this.shouldParseVariables(sourceFile.getFilePath()))
412
+ continue;
413
+ for (const varDecl of varStatement.getDeclarations()) {
414
+ if (this.shouldSkipChildNode(varDecl))
415
+ continue;
416
+ const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, { isExported }, sourceFileNode.id);
417
+ this.addNode(variableNode);
418
+ const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
419
+ this.addEdge(containsEdge);
234
420
  }
235
421
  }
236
422
  }
@@ -257,7 +443,15 @@ export class TypeScriptParser {
257
443
  for (const child of children) {
258
444
  if (this.shouldSkipChildNode(child))
259
445
  continue;
260
- const coreNode = this.createCoreNode(child, type, {}, parentNode.id);
446
+ // Track parent class name for methods, properties, and constructors
447
+ const extraProperties = {};
448
+ if (parentNode.coreType === CoreNodeType.CLASS_DECLARATION &&
449
+ (type === CoreNodeType.METHOD_DECLARATION ||
450
+ type === CoreNodeType.PROPERTY_DECLARATION ||
451
+ type === CoreNodeType.CONSTRUCTOR_DECLARATION)) {
452
+ extraProperties.parentClassName = parentNode.properties.name;
453
+ }
454
+ const coreNode = this.createCoreNode(child, type, extraProperties, parentNode.id);
261
455
  this.addNode(coreNode);
262
456
  const coreEdge = this.createCoreEdge(edgeType, parentNode.id, coreNode.id);
263
457
  this.addEdge(coreEdge);
@@ -269,6 +463,15 @@ export class TypeScriptParser {
269
463
  if (SKELETONIZE_TYPES.has(type)) {
270
464
  this.skeletonizeChildInParent(parentNode, coreNode);
271
465
  }
466
+ // Extract CALLS edges from method/function/constructor bodies
467
+ const CALL_EXTRACTION_TYPES = new Set([
468
+ CoreNodeType.METHOD_DECLARATION,
469
+ CoreNodeType.FUNCTION_DECLARATION,
470
+ CoreNodeType.CONSTRUCTOR_DECLARATION,
471
+ ]);
472
+ if (CALL_EXTRACTION_TYPES.has(type)) {
473
+ this.extractCallsFromBody(coreNode, child);
474
+ }
272
475
  const childNodeConfig = this.coreSchema.nodeTypes[type];
273
476
  if (childNodeConfig) {
274
477
  this.queueRelationshipNodes(childNodeConfig, coreNode, child);
@@ -286,87 +489,542 @@ export class TypeScriptParser {
286
489
  parent.properties.sourceCode = parent.properties.sourceCode.replace(childText, placeholder);
287
490
  }
288
491
  }
289
- /**
290
- * Queue relationship edges for deferred processing
291
- * These are resolved after all nodes are parsed since the target may not exist yet
292
- */
293
- queueRelationshipNodes(nodeConfig, parsedNode, astNode) {
294
- if (!nodeConfig.relationships || nodeConfig.relationships.length === 0)
295
- return;
296
- for (const relationship of nodeConfig.relationships) {
297
- const { edgeType, method, cardinality, targetNodeType } = relationship;
298
- const astGetter = astNode[method];
299
- if (typeof astGetter !== 'function')
300
- continue;
301
- const result = astGetter.call(astNode);
302
- if (!result)
303
- continue;
304
- const targets = cardinality === 'single' ? [result] : result;
305
- for (const target of targets) {
306
- if (!target)
307
- continue;
308
- const targetName = this.extractRelationshipTargetName(target);
309
- if (!targetName)
310
- continue;
311
- // For EXTENDS/IMPLEMENTS, try to get the file path from the resolved declaration
312
- let targetFilePath;
313
- if (edgeType === CoreEdgeType.EXTENDS || edgeType === CoreEdgeType.IMPLEMENTS) {
314
- targetFilePath = this.extractTargetFilePath(target);
315
- }
316
- this.deferredEdges.push({
317
- edgeType: edgeType,
318
- sourceNodeId: parsedNode.id,
319
- targetName,
320
- targetType: targetNodeType,
321
- targetFilePath,
322
- });
323
- }
324
- }
325
- }
326
- /**
327
- * Extract the file path from a resolved target declaration.
328
- * Used for EXTENDS/IMPLEMENTS to enable precise matching.
329
- */
330
- extractTargetFilePath(target) {
492
+ extractNodeName(astNode, coreType) {
331
493
  try {
332
- // If target is already a ClassDeclaration or InterfaceDeclaration, get its source file
333
- if (Node.isClassDeclaration(target) || Node.isInterfaceDeclaration(target)) {
334
- return target.getSourceFile().getFilePath();
335
- }
336
- // If target is ExpressionWithTypeArguments (e.g., extends Foo<T>), resolve the type
337
- if (Node.isExpressionWithTypeArguments(target)) {
338
- const expression = target.getExpression();
339
- if (Node.isIdentifier(expression)) {
340
- // Try to get the definition of the type
341
- const definitions = expression.getDefinitionNodes();
342
- for (const def of definitions) {
343
- if (Node.isClassDeclaration(def) || Node.isInterfaceDeclaration(def)) {
344
- return def.getSourceFile().getFilePath();
345
- }
346
- }
347
- }
348
- }
494
+ switch (coreType) {
495
+ case CoreNodeType.SOURCE_FILE:
496
+ if (Node.isSourceFile(astNode)) {
497
+ return astNode.getBaseName();
498
+ }
499
+ break;
500
+ case CoreNodeType.CLASS_DECLARATION:
501
+ if (Node.isClassDeclaration(astNode)) {
502
+ return astNode.getName() ?? 'AnonymousClass';
503
+ }
504
+ break;
505
+ case CoreNodeType.METHOD_DECLARATION:
506
+ if (Node.isMethodDeclaration(astNode)) {
507
+ return astNode.getName();
508
+ }
509
+ break;
510
+ case CoreNodeType.FUNCTION_DECLARATION:
511
+ if (Node.isFunctionDeclaration(astNode)) {
512
+ return astNode.getName() ?? 'AnonymousFunction';
513
+ }
514
+ break;
515
+ case CoreNodeType.INTERFACE_DECLARATION:
516
+ if (Node.isInterfaceDeclaration(astNode)) {
517
+ return astNode.getName();
518
+ }
519
+ break;
520
+ case CoreNodeType.PROPERTY_DECLARATION:
521
+ if (Node.isPropertyDeclaration(astNode)) {
522
+ return astNode.getName();
523
+ }
524
+ break;
525
+ case CoreNodeType.PARAMETER_DECLARATION:
526
+ if (Node.isParameterDeclaration(astNode)) {
527
+ return astNode.getName();
528
+ }
529
+ break;
530
+ case CoreNodeType.IMPORT_DECLARATION:
531
+ if (Node.isImportDeclaration(astNode)) {
532
+ return astNode.getModuleSpecifierValue();
533
+ }
534
+ break;
535
+ case CoreNodeType.DECORATOR:
536
+ if (Node.isDecorator(astNode)) {
537
+ return astNode.getName();
538
+ }
539
+ break;
540
+ case CoreNodeType.TYPE_ALIAS:
541
+ if (Node.isTypeAliasDeclaration(astNode)) {
542
+ return astNode.getName();
543
+ }
544
+ break;
545
+ default:
546
+ return astNode.getKindName();
547
+ }
548
+ }
549
+ catch (error) {
550
+ console.warn(`Error extracting name for ${coreType}:`, error);
551
+ }
552
+ return 'Unknown';
553
+ }
554
+ extractProperty(astNode, propDef) {
555
+ const { method, source, defaultValue } = propDef.extraction;
556
+ try {
557
+ switch (method) {
558
+ case 'ast':
559
+ if (typeof source === 'string') {
560
+ const fn = astNode[source];
561
+ return typeof fn === 'function' ? fn.call(astNode) : defaultValue;
562
+ }
563
+ return defaultValue;
564
+ case 'function':
565
+ if (typeof source === 'function') {
566
+ return source(astNode);
567
+ }
568
+ return defaultValue;
569
+ case 'static':
570
+ return defaultValue;
571
+ case 'context':
572
+ // Context properties are handled by context extractors
573
+ return undefined;
574
+ default:
575
+ return defaultValue;
576
+ }
577
+ }
578
+ catch (error) {
579
+ console.warn(`Failed to extract property ${propDef.name}:`, error);
580
+ return defaultValue;
581
+ }
582
+ }
583
+ /**
584
+ * Extract the target name from an AST node returned by relationship methods
585
+ */
586
+ extractRelationshipTargetName(target) {
587
+ if (Node.isClassDeclaration(target))
588
+ return target.getName();
589
+ if (Node.isInterfaceDeclaration(target))
590
+ return target.getName();
591
+ if (Node.isExpressionWithTypeArguments(target)) {
592
+ const expression = target.getExpression();
593
+ const text = expression.getText();
594
+ const genericIndex = text.indexOf('<');
595
+ return genericIndex > 0 ? text.substring(0, genericIndex) : text;
596
+ }
597
+ return undefined;
598
+ }
599
+ /**
600
+ * Extract the file path from a resolved target declaration.
601
+ * Used for EXTENDS/IMPLEMENTS to enable precise matching.
602
+ */
603
+ extractTargetFilePath(target) {
604
+ try {
605
+ // If target is already a ClassDeclaration or InterfaceDeclaration, get its source file
606
+ if (Node.isClassDeclaration(target) || Node.isInterfaceDeclaration(target)) {
607
+ return target.getSourceFile().getFilePath();
608
+ }
609
+ // If target is ExpressionWithTypeArguments (e.g., extends Foo<T>), resolve the type
610
+ if (Node.isExpressionWithTypeArguments(target)) {
611
+ const expression = target.getExpression();
612
+ if (Node.isIdentifier(expression)) {
613
+ // Try to get the definition of the type
614
+ const definitions = expression.getDefinitionNodes();
615
+ for (const def of definitions) {
616
+ if (Node.isClassDeclaration(def) || Node.isInterfaceDeclaration(def)) {
617
+ return def.getSourceFile().getFilePath();
618
+ }
619
+ }
620
+ }
621
+ }
349
622
  }
350
623
  catch {
351
624
  // If resolution fails (e.g., external type), return undefined
352
625
  }
353
- return undefined;
626
+ return undefined;
627
+ }
628
+ /**
629
+ * Extract method/function calls from the body of a method or function.
630
+ * Creates deferred CALLS edges for resolution after all nodes are parsed.
631
+ */
632
+ extractCallsFromBody(callerNode, astNode) {
633
+ // Get the body of the method/function
634
+ let body;
635
+ if (Node.isMethodDeclaration(astNode)) {
636
+ body = astNode.getBody();
637
+ }
638
+ else if (Node.isFunctionDeclaration(astNode)) {
639
+ body = astNode.getBody();
640
+ }
641
+ else if (Node.isConstructorDeclaration(astNode)) {
642
+ body = astNode.getBody();
643
+ }
644
+ if (!body)
645
+ return;
646
+ // Skip very short method bodies (likely simple getters/setters)
647
+ const bodyText = body.getText();
648
+ if (bodyText.length < 15)
649
+ return;
650
+ // Track unique calls to avoid duplicates (same method called multiple times)
651
+ const seenCalls = new Set();
652
+ // Traverse all descendants looking for call expressions
653
+ body.forEachDescendant((descendant) => {
654
+ // Method/function calls: foo(), this.foo(), obj.foo()
655
+ if (Node.isCallExpression(descendant)) {
656
+ const callInfo = this.extractCallInfo(descendant, callerNode);
657
+ if (callInfo) {
658
+ const callKey = `${callInfo.targetName}:${callInfo.receiverType ?? 'unknown'}`;
659
+ if (!seenCalls.has(callKey)) {
660
+ seenCalls.add(callKey);
661
+ this.queueCallEdge(callerNode.id, callInfo);
662
+ }
663
+ }
664
+ }
665
+ // Constructor calls: new ClassName()
666
+ if (Node.isNewExpression(descendant)) {
667
+ const callInfo = this.extractConstructorCallInfo(descendant);
668
+ if (callInfo) {
669
+ const callKey = `constructor:${callInfo.targetClassName}`;
670
+ if (!seenCalls.has(callKey)) {
671
+ seenCalls.add(callKey);
672
+ this.queueCallEdge(callerNode.id, callInfo);
673
+ }
674
+ }
675
+ }
676
+ });
677
+ }
678
+ /**
679
+ * Extract call information from a CallExpression.
680
+ */
681
+ extractCallInfo(callExpr, callerNode) {
682
+ if (!Node.isCallExpression(callExpr))
683
+ return null;
684
+ const expression = callExpr.getExpression();
685
+ const lineNumber = callExpr.getStartLineNumber();
686
+ const argumentCount = callExpr.getArguments().length;
687
+ // Check if this call is awaited
688
+ const parent = callExpr.getParent();
689
+ const isAsync = parent !== undefined && Node.isAwaitExpression(parent);
690
+ // Case 1: Direct function call - functionName()
691
+ if (Node.isIdentifier(expression)) {
692
+ const targetName = expression.getText();
693
+ // Skip built-in functions and common utilities
694
+ if (BUILT_IN_FUNCTIONS.has(targetName))
695
+ return null;
696
+ return {
697
+ targetName,
698
+ targetType: CoreNodeType.FUNCTION_DECLARATION,
699
+ lineNumber,
700
+ isAsync,
701
+ argumentCount,
702
+ };
703
+ }
704
+ // Case 2: Method call - obj.method() or this.method() or this.service.method()
705
+ if (Node.isPropertyAccessExpression(expression)) {
706
+ const methodName = expression.getName();
707
+ const receiver = expression.getExpression();
708
+ // Skip common built-in method calls
709
+ if (BUILT_IN_METHODS.has(methodName))
710
+ return null;
711
+ // this.method() - internal class method call
712
+ if (Node.isThisExpression(receiver)) {
713
+ return {
714
+ targetName: methodName,
715
+ targetType: CoreNodeType.METHOD_DECLARATION,
716
+ receiverExpression: 'this',
717
+ lineNumber,
718
+ isAsync,
719
+ argumentCount,
720
+ };
721
+ }
722
+ // this.service.method() - dependency injection call
723
+ if (Node.isPropertyAccessExpression(receiver)) {
724
+ const innerReceiver = receiver.getExpression();
725
+ if (Node.isThisExpression(innerReceiver)) {
726
+ const propertyName = receiver.getName();
727
+ // Try to resolve the type from constructor parameters
728
+ const receiverType = this.resolvePropertyType(callerNode, propertyName);
729
+ return {
730
+ targetName: methodName,
731
+ targetType: CoreNodeType.METHOD_DECLARATION,
732
+ receiverExpression: `this.${propertyName}`,
733
+ receiverPropertyName: propertyName,
734
+ receiverType,
735
+ lineNumber,
736
+ isAsync,
737
+ argumentCount,
738
+ };
739
+ }
740
+ }
741
+ // variable.method() - method call on local variable
742
+ if (Node.isIdentifier(receiver)) {
743
+ const varName = receiver.getText();
744
+ // Try to get the type of the variable
745
+ let receiverType;
746
+ try {
747
+ const typeText = receiver.getType().getText();
748
+ // Clean up type (remove generics, imports)
749
+ receiverType = cleanTypeName(typeText);
750
+ }
751
+ catch {
752
+ // Type resolution failed
753
+ }
754
+ return {
755
+ targetName: methodName,
756
+ targetType: CoreNodeType.METHOD_DECLARATION,
757
+ receiverExpression: varName,
758
+ receiverType,
759
+ lineNumber,
760
+ isAsync,
761
+ argumentCount,
762
+ };
763
+ }
764
+ }
765
+ return null;
766
+ }
767
+ /**
768
+ * Extract constructor call information from a NewExpression.
769
+ */
770
+ extractConstructorCallInfo(newExpr) {
771
+ if (!Node.isNewExpression(newExpr))
772
+ return null;
773
+ const expression = newExpr.getExpression();
774
+ const lineNumber = newExpr.getStartLineNumber();
775
+ const argumentCount = newExpr.getArguments().length;
776
+ // new ClassName()
777
+ if (Node.isIdentifier(expression)) {
778
+ const className = expression.getText();
779
+ // Skip built-in constructors
780
+ if (BUILT_IN_CLASSES.has(className))
781
+ return null;
782
+ return {
783
+ targetName: 'constructor',
784
+ targetType: CoreNodeType.CONSTRUCTOR_DECLARATION,
785
+ targetClassName: className,
786
+ lineNumber,
787
+ isAsync: false,
788
+ argumentCount,
789
+ };
790
+ }
791
+ return null;
792
+ }
793
+ /**
794
+ * Queue a CALLS edge for deferred resolution.
795
+ */
796
+ queueCallEdge(sourceNodeId, callInfo) {
797
+ // For constructor calls, use class name as target
798
+ const targetName = callInfo.targetClassName ?? callInfo.targetName;
799
+ this.deferredEdges.push({
800
+ edgeType: CoreEdgeType.CALLS,
801
+ sourceNodeId,
802
+ targetName,
803
+ targetType: callInfo.targetType,
804
+ callContext: {
805
+ receiverExpression: callInfo.receiverExpression,
806
+ receiverType: callInfo.receiverType,
807
+ receiverPropertyName: callInfo.receiverPropertyName,
808
+ lineNumber: callInfo.lineNumber,
809
+ isAsync: callInfo.isAsync,
810
+ argumentCount: callInfo.argumentCount,
811
+ },
812
+ });
813
+ }
814
+ /**
815
+ * Resolve the type of a class property from constructor parameters.
816
+ * Used for NestJS dependency injection pattern.
817
+ */
818
+ resolvePropertyType(node, propertyName) {
819
+ // Look for constructor parameter types in the node's context
820
+ const context = node.properties.context;
821
+ if (context?.constructorParamTypes) {
822
+ const paramTypes = context.constructorParamTypes;
823
+ // Constructor params with 'private' or 'public' become properties
824
+ // Try to find a matching parameter by name
825
+ // The context extractor stores types in order, we need to match by name
826
+ // For now, use a heuristic: look for type that matches property name
827
+ for (const paramType of paramTypes) {
828
+ // Check if the type name matches (case-insensitive, removing 'Service', 'Repository' etc.)
829
+ const normalizedProp = propertyName.toLowerCase().replace(/service|repository|provider/gi, '');
830
+ const normalizedType = paramType.toLowerCase().replace(/service|repository|provider/gi, '');
831
+ if (normalizedType.includes(normalizedProp) || normalizedProp.includes(normalizedType)) {
832
+ return paramType;
833
+ }
834
+ }
835
+ }
836
+ // Also check the class node's parent for constructor info
837
+ const classNode = this.findParentClassNode(node);
838
+ if (classNode?.properties.context?.constructorParamTypes) {
839
+ const paramTypes = classNode.properties.context.constructorParamTypes;
840
+ for (const paramType of paramTypes) {
841
+ const normalizedProp = propertyName.toLowerCase().replace(/service|repository|provider/gi, '');
842
+ const normalizedType = paramType.toLowerCase().replace(/service|repository|provider/gi, '');
843
+ if (normalizedType.includes(normalizedProp) || normalizedProp.includes(normalizedType)) {
844
+ return paramType;
845
+ }
846
+ }
847
+ }
848
+ return undefined;
849
+ }
850
+ createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
851
+ const name = this.extractNodeName(astNode, coreType);
852
+ const filePath = astNode.getSourceFile().getFilePath();
853
+ const nodeId = generateDeterministicId(this.projectId, coreType, filePath, name, parentId);
854
+ // Extract base properties using schema
855
+ const properties = {
856
+ id: nodeId,
857
+ projectId: this.projectId,
858
+ name,
859
+ coreType,
860
+ filePath,
861
+ startLine: astNode.getStartLineNumber(),
862
+ endLine: astNode.getEndLineNumber(),
863
+ sourceCode: astNode.getText(),
864
+ createdAt: new Date().toISOString(),
865
+ ...baseProperties,
866
+ };
867
+ // Extract schema-defined properties
868
+ const coreNodeDef = this.coreSchema.nodeTypes[coreType];
869
+ if (coreNodeDef) {
870
+ for (const propDef of coreNodeDef.properties) {
871
+ try {
872
+ const value = this.extractProperty(astNode, propDef);
873
+ if (value !== undefined && propDef.name !== 'context') {
874
+ properties[propDef.name] = value;
875
+ }
876
+ }
877
+ catch (error) {
878
+ console.warn(`Failed to extract core property ${propDef.name}:`, error);
879
+ }
880
+ }
881
+ }
882
+ // Compute normalizedHash for duplicate detection (methods, functions, constructors)
883
+ if (coreType === CoreNodeType.METHOD_DECLARATION ||
884
+ coreType === CoreNodeType.FUNCTION_DECLARATION ||
885
+ coreType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
886
+ try {
887
+ const { normalizedHash } = normalizeCode(properties.sourceCode);
888
+ if (normalizedHash) {
889
+ properties.normalizedHash = normalizedHash;
890
+ }
891
+ }
892
+ catch (error) {
893
+ console.warn(`Failed to compute normalizedHash for ${nodeId}:`, error);
894
+ }
895
+ }
896
+ return {
897
+ id: nodeId,
898
+ coreType,
899
+ labels: [...(coreNodeDef?.neo4j.labels || [])],
900
+ properties,
901
+ sourceNode: astNode,
902
+ skipEmbedding: coreNodeDef?.neo4j.skipEmbedding ?? false,
903
+ };
904
+ }
905
+ createCoreEdge(relationshipType, sourceNodeId, targetNodeId) {
906
+ return toParsedEdge(createCoreEdgeFactory({
907
+ edgeType: relationshipType,
908
+ sourceNodeId,
909
+ targetNodeId,
910
+ projectId: this.projectId,
911
+ }));
912
+ }
913
+ createCallsEdge(sourceNodeId, targetNodeId, callContext) {
914
+ return toParsedEdge(createCallsEdgeFactory({
915
+ sourceNodeId,
916
+ targetNodeId,
917
+ projectId: this.projectId,
918
+ callContext,
919
+ }));
920
+ }
921
+ createFrameworkEdge(semanticType, relationshipType, sourceNodeId, targetNodeId, context = {}, relationshipWeight = 0.5) {
922
+ const { id, properties } = createFrameworkEdgeData({
923
+ semanticType,
924
+ sourceNodeId,
925
+ targetNodeId,
926
+ projectId: this.projectId,
927
+ context,
928
+ relationshipWeight,
929
+ });
930
+ return {
931
+ id,
932
+ relationshipType,
933
+ sourceNodeId,
934
+ targetNodeId,
935
+ properties,
936
+ };
937
+ }
938
+ /**
939
+ * Queue relationship edges for deferred processing
940
+ * These are resolved after all nodes are parsed since the target may not exist yet
941
+ */
942
+ queueRelationshipNodes(nodeConfig, parsedNode, astNode) {
943
+ if (!nodeConfig.relationships || nodeConfig.relationships.length === 0)
944
+ return;
945
+ for (const relationship of nodeConfig.relationships) {
946
+ const { edgeType, method, cardinality, targetNodeType } = relationship;
947
+ const astGetter = astNode[method];
948
+ if (typeof astGetter !== 'function')
949
+ continue;
950
+ const result = astGetter.call(astNode);
951
+ if (!result)
952
+ continue;
953
+ const targets = cardinality === 'single' ? [result] : result;
954
+ for (const target of targets) {
955
+ if (!target)
956
+ continue;
957
+ const targetName = this.extractRelationshipTargetName(target);
958
+ if (!targetName)
959
+ continue;
960
+ // For EXTENDS/IMPLEMENTS, try to get the file path from the resolved declaration
961
+ let targetFilePath;
962
+ if (edgeType === CoreEdgeType.EXTENDS || edgeType === CoreEdgeType.IMPLEMENTS) {
963
+ targetFilePath = this.extractTargetFilePath(target);
964
+ }
965
+ this.deferredEdges.push({
966
+ edgeType: edgeType,
967
+ sourceNodeId: parsedNode.id,
968
+ targetName,
969
+ targetType: targetNodeType,
970
+ targetFilePath,
971
+ });
972
+ }
973
+ }
354
974
  }
355
975
  /**
356
- * Extract the target name from an AST node returned by relationship methods
976
+ * Resolve the target node for a CALLS edge.
977
+ * Uses special resolution logic based on call context.
357
978
  */
358
- extractRelationshipTargetName(target) {
359
- if (Node.isClassDeclaration(target))
360
- return target.getName();
361
- if (Node.isInterfaceDeclaration(target))
362
- return target.getName();
363
- if (Node.isExpressionWithTypeArguments(target)) {
364
- const expression = target.getExpression();
365
- const text = expression.getText();
366
- const genericIndex = text.indexOf('<');
367
- return genericIndex > 0 ? text.substring(0, genericIndex) : text;
979
+ resolveCallTarget(deferred) {
980
+ const { targetName, targetType, sourceNodeId, callContext } = deferred;
981
+ // Case 1: this.method() - internal class method call
982
+ if (callContext?.receiverExpression === 'this') {
983
+ // Find the caller's class, then look for method in same class
984
+ const sourceNode = this.parsedNodes.get(sourceNodeId) ?? this.existingNodes.get(sourceNodeId);
985
+ if (sourceNode) {
986
+ const className = this.getClassNameForNode(sourceNode);
987
+ if (className && this.methodsByClass.has(className)) {
988
+ const method = this.methodsByClass.get(className).get(targetName);
989
+ if (method)
990
+ return method;
991
+ }
992
+ }
368
993
  }
369
- return undefined;
994
+ // Case 2: this.service.method() - dependency injection call
995
+ if (callContext?.receiverType) {
996
+ // Look for method in the receiver's class
997
+ const className = callContext.receiverType;
998
+ if (this.methodsByClass.has(className)) {
999
+ const method = this.methodsByClass.get(className).get(targetName);
1000
+ if (method)
1001
+ return method;
1002
+ }
1003
+ // Fallback: search all nodes for a method with this name in a class matching the type
1004
+ for (const [, node] of this.parsedNodes) {
1005
+ if (node.coreType === CoreNodeType.METHOD_DECLARATION && node.properties.name === targetName) {
1006
+ // Check if parent class matches the receiver type
1007
+ const parentClass = this.findParentClassNode(node);
1008
+ if (parentClass?.properties.name === className) {
1009
+ return node;
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ // Case 3: Constructor call - find the constructor in the target class
1015
+ if (targetType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
1016
+ const constructor = this.constructorsByClass.get(targetName);
1017
+ if (constructor)
1018
+ return constructor;
1019
+ }
1020
+ // Case 4: Standalone function call
1021
+ if (targetType === CoreNodeType.FUNCTION_DECLARATION) {
1022
+ const func = this.functionsByName.get(targetName);
1023
+ if (func)
1024
+ return func;
1025
+ }
1026
+ // Fallback: generic name matching
1027
+ return this.findNodeByNameAndType(targetName, targetType);
370
1028
  }
371
1029
  /**
372
1030
  * Find a parsed node by name and core type
@@ -464,26 +1122,49 @@ export class TypeScriptParser {
464
1122
  return undefined;
465
1123
  }
466
1124
  /**
467
- * Resolve deferred edges after all nodes have been parsed
1125
+ * Resolve deferred edges against both parsed nodes and existing nodes.
1126
+ * Call this after all chunks have been parsed.
1127
+ * @returns Resolved edges
468
1128
  */
469
1129
  async resolveDeferredEdges() {
1130
+ const resolvedEdges = [];
470
1131
  // Count edges by type for logging
471
1132
  const importsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.IMPORTS).length;
472
1133
  const extendsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.EXTENDS).length;
473
1134
  const implementsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.IMPLEMENTS).length;
1135
+ const callsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.CALLS).length;
474
1136
  let importsResolved = 0;
475
1137
  let extendsResolved = 0;
476
1138
  let implementsResolved = 0;
1139
+ let callsResolved = 0;
477
1140
  const unresolvedImports = [];
478
1141
  const unresolvedExtends = [];
479
1142
  const unresolvedImplements = [];
1143
+ const unresolvedCalls = [];
480
1144
  for (const deferred of this.deferredEdges) {
1145
+ // Special handling for CALLS edges - uses resolveCallTarget for proper resolution
1146
+ if (deferred.edgeType === CoreEdgeType.CALLS) {
1147
+ const targetNode = this.resolveCallTarget(deferred);
1148
+ if (targetNode) {
1149
+ const edge = this.createCallsEdge(deferred.sourceNodeId, targetNode.id, deferred.callContext);
1150
+ resolvedEdges.push(edge);
1151
+ this.addEdge(edge);
1152
+ callsResolved++;
1153
+ }
1154
+ else {
1155
+ const callDesc = deferred.callContext?.receiverType
1156
+ ? `${deferred.callContext.receiverType}.${deferred.targetName}`
1157
+ : deferred.targetName;
1158
+ unresolvedCalls.push(callDesc);
1159
+ }
1160
+ continue;
1161
+ }
481
1162
  // Pass filePath for precise matching (especially important for EXTENDS/IMPLEMENTS)
482
1163
  const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
483
1164
  if (targetNode) {
484
1165
  const edge = this.createCoreEdge(deferred.edgeType, deferred.sourceNodeId, targetNode.id);
1166
+ resolvedEdges.push(edge);
485
1167
  this.addEdge(edge);
486
- // Track resolution by type
487
1168
  if (deferred.edgeType === CoreEdgeType.IMPORTS) {
488
1169
  importsResolved++;
489
1170
  }
@@ -495,7 +1176,6 @@ export class TypeScriptParser {
495
1176
  }
496
1177
  }
497
1178
  else {
498
- // Track unresolved by type
499
1179
  if (deferred.edgeType === CoreEdgeType.IMPORTS) {
500
1180
  unresolvedImports.push(deferred.targetName);
501
1181
  }
@@ -507,208 +1187,40 @@ export class TypeScriptParser {
507
1187
  }
508
1188
  }
509
1189
  }
510
- // Log import resolution stats
511
- if (importsCount > 0) {
512
- await debugLog('Import edge resolution', {
513
- totalImports: importsCount,
1190
+ // Log edge resolution stats
1191
+ await debugLog('Edge resolution', {
1192
+ totalDeferredEdges: this.deferredEdges.length,
1193
+ totalNodesAvailable: this.parsedNodes.size + this.existingNodes.size,
1194
+ imports: {
1195
+ queued: importsCount,
514
1196
  resolved: importsResolved,
515
- unresolvedCount: unresolvedImports.length,
516
- unresolvedSample: unresolvedImports.slice(0, 10),
517
- });
518
- }
519
- // Log inheritance (EXTENDS/IMPLEMENTS) resolution stats
520
- if (extendsCount > 0 || implementsCount > 0) {
521
- await debugLog('Inheritance edge resolution', {
522
- extendsQueued: extendsCount,
523
- extendsResolved,
524
- extendsUnresolved: unresolvedExtends.length,
525
- unresolvedExtendsSample: unresolvedExtends.slice(0, 10),
526
- implementsQueued: implementsCount,
527
- implementsResolved,
528
- implementsUnresolved: unresolvedImplements.length,
529
- unresolvedImplementsSample: unresolvedImplements.slice(0, 10),
530
- });
531
- }
1197
+ unresolved: unresolvedImports.length,
1198
+ sample: unresolvedImports.slice(0, 10),
1199
+ },
1200
+ extends: {
1201
+ queued: extendsCount,
1202
+ resolved: extendsResolved,
1203
+ unresolved: unresolvedExtends.length,
1204
+ sample: unresolvedExtends.slice(0, 10),
1205
+ },
1206
+ implements: {
1207
+ queued: implementsCount,
1208
+ resolved: implementsResolved,
1209
+ unresolved: unresolvedImplements.length,
1210
+ sample: unresolvedImplements.slice(0, 10),
1211
+ },
1212
+ calls: {
1213
+ queued: callsCount,
1214
+ resolved: callsResolved,
1215
+ unresolved: unresolvedCalls.length,
1216
+ sample: unresolvedCalls.slice(0, 10),
1217
+ },
1218
+ });
532
1219
  this.deferredEdges = [];
533
- }
534
- async parseCoreTypeScript(sourceFile) {
535
- try {
536
- // Create source file node
537
- const sourceFileNode = this.createCoreNode(sourceFile, CoreNodeType.SOURCE_FILE);
538
- this.addNode(sourceFileNode);
539
- // Parse classes
540
- for (const classDecl of sourceFile.getClasses()) {
541
- const classNode = this.createCoreNode(classDecl, CoreNodeType.CLASS_DECLARATION, {}, sourceFileNode.id);
542
- this.addNode(classNode);
543
- // File contains class relationship
544
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, classNode.id);
545
- this.addEdge(containsEdge);
546
- // Parse class decorators
547
- for (const decorator of classDecl.getDecorators()) {
548
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, classNode.id);
549
- this.addNode(decoratorNode);
550
- // Class decorated with decorator relationship
551
- const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, classNode.id, decoratorNode.id);
552
- this.addEdge(decoratedEdge);
553
- }
554
- // Parse methods
555
- for (const method of classDecl.getMethods()) {
556
- const methodNode = this.createCoreNode(method, CoreNodeType.METHOD_DECLARATION, {}, classNode.id);
557
- this.addNode(methodNode);
558
- // Class has method relationship
559
- const hasMethodEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, methodNode.id);
560
- this.addEdge(hasMethodEdge);
561
- // Parse method decorators
562
- for (const decorator of method.getDecorators()) {
563
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, methodNode.id);
564
- this.addNode(decoratorNode);
565
- // Method decorated with decorator relationship
566
- const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, methodNode.id, decoratorNode.id);
567
- this.addEdge(decoratedEdge);
568
- }
569
- // Parse method parameters
570
- for (const param of method.getParameters()) {
571
- const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, methodNode.id);
572
- this.addNode(paramNode);
573
- // Method has parameter relationship
574
- const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, methodNode.id, paramNode.id);
575
- this.addEdge(hasParamEdge);
576
- // Parse parameter decorators
577
- for (const decorator of param.getDecorators()) {
578
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, paramNode.id);
579
- this.addNode(decoratorNode);
580
- // Parameter decorated with decorator relationship
581
- const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, paramNode.id, decoratorNode.id);
582
- this.addEdge(decoratedEdge);
583
- }
584
- }
585
- }
586
- // Parse properties
587
- for (const property of classDecl.getProperties()) {
588
- const propertyNode = this.createCoreNode(property, CoreNodeType.PROPERTY_DECLARATION, {}, classNode.id);
589
- this.addNode(propertyNode);
590
- // Class has property relationship
591
- const hasPropertyEdge = this.createCoreEdge(CoreEdgeType.HAS_MEMBER, classNode.id, propertyNode.id);
592
- this.addEdge(hasPropertyEdge);
593
- // Parse property decorators
594
- for (const decorator of property.getDecorators()) {
595
- const decoratorNode = this.createCoreNode(decorator, CoreNodeType.DECORATOR, {}, propertyNode.id);
596
- this.addNode(decoratorNode);
597
- // Property decorated with decorator relationship
598
- const decoratedEdge = this.createCoreEdge(CoreEdgeType.DECORATED_WITH, propertyNode.id, decoratorNode.id);
599
- this.addEdge(decoratedEdge);
600
- }
601
- }
602
- }
603
- // Parse interfaces
604
- for (const interfaceDecl of sourceFile.getInterfaces()) {
605
- const interfaceNode = this.createCoreNode(interfaceDecl, CoreNodeType.INTERFACE_DECLARATION, {}, sourceFileNode.id);
606
- this.addNode(interfaceNode);
607
- // File contains interface relationship
608
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, interfaceNode.id);
609
- this.addEdge(containsEdge);
610
- }
611
- // Parse functions
612
- for (const funcDecl of sourceFile.getFunctions()) {
613
- const functionNode = this.createCoreNode(funcDecl, CoreNodeType.FUNCTION_DECLARATION, {}, sourceFileNode.id);
614
- this.addNode(functionNode);
615
- // File contains function relationship
616
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, functionNode.id);
617
- this.addEdge(containsEdge);
618
- // Parse function parameters
619
- for (const param of funcDecl.getParameters()) {
620
- const paramNode = this.createCoreNode(param, CoreNodeType.PARAMETER_DECLARATION, {}, functionNode.id);
621
- this.addNode(paramNode);
622
- // Function has parameter relationship
623
- const hasParamEdge = this.createCoreEdge(CoreEdgeType.HAS_PARAMETER, functionNode.id, paramNode.id);
624
- this.addEdge(hasParamEdge);
625
- }
626
- }
627
- // Parse imports
628
- for (const importDecl of sourceFile.getImportDeclarations()) {
629
- const importNode = this.createCoreNode(importDecl, CoreNodeType.IMPORT_DECLARATION, {}, sourceFileNode.id);
630
- this.addNode(importNode);
631
- // File contains import relationship
632
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, importNode.id);
633
- this.addEdge(containsEdge);
634
- // Try to resolve import to create SourceFile -> SourceFile IMPORTS edge
635
- try {
636
- const targetSourceFile = importDecl.getModuleSpecifierSourceFile();
637
- if (targetSourceFile) {
638
- const targetFilePath = targetSourceFile.getFilePath();
639
- // Queue deferred edge - will be resolved after all files are parsed
640
- this.deferredEdges.push({
641
- edgeType: CoreEdgeType.IMPORTS,
642
- sourceNodeId: sourceFileNode.id,
643
- targetName: targetFilePath, // Use file path as "name" for SourceFiles
644
- targetType: CoreNodeType.SOURCE_FILE,
645
- });
646
- }
647
- }
648
- catch {
649
- // Module resolution failed - external dependency, skip
650
- }
651
- }
652
- // Parse variable declarations if framework schema specifies this file should have them parsed
653
- if (this.shouldParseVariables(sourceFile.getFilePath())) {
654
- for (const varStatement of sourceFile.getVariableStatements()) {
655
- for (const varDecl of varStatement.getDeclarations()) {
656
- const variableNode = this.createCoreNode(varDecl, CoreNodeType.VARIABLE_DECLARATION, {}, sourceFileNode.id);
657
- this.addNode(variableNode);
658
- // File contains variable relationship
659
- const containsEdge = this.createCoreEdge(CoreEdgeType.CONTAINS, sourceFileNode.id, variableNode.id);
660
- this.addEdge(containsEdge);
661
- }
662
- }
663
- }
664
- }
665
- catch (error) {
666
- console.error(`Error parsing file ${sourceFile.getFilePath()}:`, error);
667
- }
668
- }
669
- createCoreNode(astNode, coreType, baseProperties = {}, parentId) {
670
- const name = this.extractNodeName(astNode, coreType);
671
- const filePath = astNode.getSourceFile().getFilePath();
672
- const nodeId = generateDeterministicId(this.projectId, coreType, filePath, name, parentId);
673
- // Extract base properties using schema
674
- const properties = {
675
- id: nodeId,
676
- projectId: this.projectId,
677
- name,
678
- coreType,
679
- filePath,
680
- startLine: astNode.getStartLineNumber(),
681
- endLine: astNode.getEndLineNumber(),
682
- sourceCode: astNode.getText(),
683
- createdAt: new Date().toISOString(),
684
- ...baseProperties,
685
- };
686
- // Extract schema-defined properties
687
- const coreNodeDef = this.coreSchema.nodeTypes[coreType];
688
- if (coreNodeDef) {
689
- for (const propDef of coreNodeDef.properties) {
690
- try {
691
- const value = this.extractProperty(astNode, propDef);
692
- if (value !== undefined && propDef.name !== 'context') {
693
- properties[propDef.name] = value;
694
- }
695
- }
696
- catch (error) {
697
- console.warn(`Failed to extract core property ${propDef.name}:`, error);
698
- }
699
- }
700
- }
701
- return {
702
- id: nodeId,
703
- coreType,
704
- labels: [...(coreNodeDef?.neo4j.labels || [])],
705
- properties,
706
- sourceNode: astNode,
707
- skipEmbedding: coreNodeDef?.neo4j.skipEmbedding ?? false,
708
- };
1220
+ return resolvedEdges.map(toNeo4jEdge);
709
1221
  }
710
1222
  async applyContextExtractors() {
711
- console.log('🔧 Applying context extractors...');
1223
+ debugLog('Applying context extractors');
712
1224
  // Apply global context extractors from framework schemas
713
1225
  for (const frameworkSchema of this.frameworkSchemas) {
714
1226
  for (const extractor of frameworkSchema.contextExtractors) {
@@ -736,37 +1248,11 @@ export class TypeScriptParser {
736
1248
  }
737
1249
  }
738
1250
  }
739
- createCoreEdge(relationshipType, sourceNodeId, targetNodeId) {
740
- // Get the weight from the core schema
741
- const coreEdgeSchema = CORE_TYPESCRIPT_SCHEMA.edgeTypes[relationshipType];
742
- const relationshipWeight = coreEdgeSchema?.relationshipWeight ?? 0.5;
743
- // Generate deterministic edge ID based on type + source + target
744
- const edgeIdentity = `${relationshipType}::${sourceNodeId}::${targetNodeId}`;
745
- const edgeHash = crypto.createHash('sha256').update(edgeIdentity).digest('hex').substring(0, 16);
746
- const edgeId = `${relationshipType}:${edgeHash}`;
747
- return {
748
- id: edgeId,
749
- relationshipType,
750
- sourceNodeId,
751
- targetNodeId,
752
- properties: {
753
- coreType: relationshipType,
754
- projectId: this.projectId,
755
- source: 'ast',
756
- confidence: 1.0,
757
- relationshipWeight,
758
- filePath: '',
759
- createdAt: new Date().toISOString(),
760
- },
761
- };
762
- }
763
1251
  async applyFrameworkEnhancements() {
764
- console.log('🎯 Starting framework enhancements...');
1252
+ await debugLog('Applying framework enhancements', { schemas: this.frameworkSchemas.map((s) => s.name) });
765
1253
  for (const frameworkSchema of this.frameworkSchemas) {
766
- console.log(`📦 Applying framework schema: ${frameworkSchema.name}`);
767
1254
  await this.applyFrameworkSchema(frameworkSchema);
768
1255
  }
769
- console.log('✅ Framework enhancements complete');
770
1256
  }
771
1257
  async applyFrameworkSchema(schema) {
772
1258
  // Sort enhancements by priority (highest first)
@@ -798,7 +1284,11 @@ export class TypeScriptParser {
798
1284
  }
799
1285
  case 'function':
800
1286
  if (typeof pattern.pattern === 'function') {
801
- return pattern.pattern(node);
1287
+ // Pass the AST sourceNode to pattern functions, not the ParsedNode wrapper
1288
+ const astNode = node.sourceNode;
1289
+ if (!astNode)
1290
+ return false;
1291
+ return pattern.pattern(astNode);
802
1292
  }
803
1293
  return false;
804
1294
  case 'classname':
@@ -867,7 +1357,7 @@ export class TypeScriptParser {
867
1357
  }
868
1358
  }
869
1359
  async applyEdgeEnhancements() {
870
- console.log('🔗 Applying edge enhancements...');
1360
+ await debugLog('Applying edge enhancements');
871
1361
  for (const frameworkSchema of this.frameworkSchemas) {
872
1362
  for (const edgeEnhancement of Object.values(frameworkSchema.edgeEnhancements)) {
873
1363
  await this.applyEdgeEnhancement(edgeEnhancement);
@@ -901,165 +1391,110 @@ export class TypeScriptParser {
901
1391
  console.error(`Error applying edge enhancement ${edgeEnhancement.name}:`, error);
902
1392
  }
903
1393
  }
904
- createFrameworkEdge(semanticType, relationshipType, sourceNodeId, targetNodeId, context = {}, relationshipWeight = 0.5) {
905
- const { id, properties } = createFrameworkEdgeData({
906
- semanticType,
907
- sourceNodeId,
908
- targetNodeId,
909
- projectId: this.projectId,
910
- context,
911
- relationshipWeight,
912
- });
913
- return {
914
- id,
915
- relationshipType,
916
- sourceNodeId,
917
- targetNodeId,
918
- properties,
919
- };
1394
+ /**
1395
+ * Apply edge enhancements on all accumulated nodes.
1396
+ * Call this after all chunks have been parsed for streaming mode.
1397
+ * This allows context-dependent edges (like INTERNAL_API_CALL) to be detected
1398
+ * after all nodes and their context have been collected.
1399
+ * @returns New edges created by edge enhancements
1400
+ */
1401
+ async applyEdgeEnhancementsManually() {
1402
+ const edgeCountBefore = this.parsedEdges.size;
1403
+ await this.applyEdgeEnhancements();
1404
+ const newEdgeCount = this.parsedEdges.size - edgeCountBefore;
1405
+ await debugLog('Edge enhancements applied', { nodeCount: this.parsedNodes.size, newEdges: newEdgeCount });
1406
+ // Return only the new edges (those created by edge enhancements)
1407
+ const allEdges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
1408
+ return allEdges.slice(edgeCountBefore);
920
1409
  }
921
- extractProperty(astNode, propDef) {
922
- const { method, source, defaultValue } = propDef.extraction;
923
- try {
924
- switch (method) {
925
- case 'ast':
926
- if (typeof source === 'string') {
927
- const fn = astNode[source];
928
- return typeof fn === 'function' ? fn.call(astNode) : defaultValue;
929
- }
930
- return defaultValue;
931
- case 'function':
932
- if (typeof source === 'function') {
933
- return source(astNode);
934
- }
935
- return defaultValue;
936
- case 'static':
937
- return defaultValue;
938
- case 'context':
939
- // Context properties are handled by context extractors
940
- return undefined;
941
- default:
942
- return defaultValue;
1410
+ addNode(node) {
1411
+ this.parsedNodes.set(node.id, node);
1412
+ this.indexNodeForCallsResolution(node);
1413
+ }
1414
+ /**
1415
+ * Build lookup indexes for efficient CALLS edge resolution.
1416
+ * Called when adding nodes during parsing or from external sources.
1417
+ */
1418
+ indexNodeForCallsResolution(node) {
1419
+ if (node.coreType === CoreNodeType.METHOD_DECLARATION) {
1420
+ const className = this.getClassNameForNode(node);
1421
+ if (className) {
1422
+ if (!this.methodsByClass.has(className)) {
1423
+ this.methodsByClass.set(className, new Map());
1424
+ }
1425
+ this.methodsByClass.get(className).set(node.properties.name, node);
943
1426
  }
944
1427
  }
945
- catch (error) {
946
- console.warn(`Failed to extract property ${propDef.name}:`, error);
947
- return defaultValue;
1428
+ else if (node.coreType === CoreNodeType.FUNCTION_DECLARATION) {
1429
+ this.functionsByName.set(node.properties.name, node);
948
1430
  }
949
- }
950
- extractNodeName(astNode, coreType) {
951
- try {
952
- switch (coreType) {
953
- case CoreNodeType.SOURCE_FILE:
954
- if (Node.isSourceFile(astNode)) {
955
- return astNode.getBaseName();
956
- }
957
- break;
958
- case CoreNodeType.CLASS_DECLARATION:
959
- if (Node.isClassDeclaration(astNode)) {
960
- return astNode.getName() ?? 'AnonymousClass';
961
- }
962
- break;
963
- case CoreNodeType.METHOD_DECLARATION:
964
- if (Node.isMethodDeclaration(astNode)) {
965
- return astNode.getName();
966
- }
967
- break;
968
- case CoreNodeType.FUNCTION_DECLARATION:
969
- if (Node.isFunctionDeclaration(astNode)) {
970
- return astNode.getName() ?? 'AnonymousFunction';
971
- }
972
- break;
973
- case CoreNodeType.INTERFACE_DECLARATION:
974
- if (Node.isInterfaceDeclaration(astNode)) {
975
- return astNode.getName();
976
- }
977
- break;
978
- case CoreNodeType.PROPERTY_DECLARATION:
979
- if (Node.isPropertyDeclaration(astNode)) {
980
- return astNode.getName();
981
- }
982
- break;
983
- case CoreNodeType.PARAMETER_DECLARATION:
984
- if (Node.isParameterDeclaration(astNode)) {
985
- return astNode.getName();
986
- }
987
- break;
988
- case CoreNodeType.IMPORT_DECLARATION:
989
- if (Node.isImportDeclaration(astNode)) {
990
- return astNode.getModuleSpecifierValue();
991
- }
992
- break;
993
- case CoreNodeType.DECORATOR:
994
- if (Node.isDecorator(astNode)) {
995
- return astNode.getName();
996
- }
997
- break;
998
- default:
999
- return astNode.getKindName();
1431
+ else if (node.coreType === CoreNodeType.CONSTRUCTOR_DECLARATION) {
1432
+ const className = this.getClassNameForNode(node);
1433
+ if (className) {
1434
+ this.constructorsByClass.set(className, node);
1000
1435
  }
1001
1436
  }
1002
- catch (error) {
1003
- console.warn(`Error extracting name for ${coreType}:`, error);
1004
- }
1005
- return 'Unknown';
1006
1437
  }
1007
- shouldSkipChildNode(node) {
1008
- const excludedNodeTypes = this.parseConfig.excludedNodeTypes ?? [];
1009
- return excludedNodeTypes.includes(node.getKindName());
1438
+ addEdge(edge) {
1439
+ this.parsedEdges.set(edge.id, edge);
1010
1440
  }
1011
1441
  /**
1012
- * Safely test if a file path matches a pattern (string or regex).
1013
- * Falls back to literal string matching if the pattern is an invalid regex.
1442
+ * Get the class name for a node.
1443
+ * Uses the parentClassName property tracked during parsing.
1014
1444
  */
1015
- matchesPattern(filePath, pattern) {
1016
- // First try literal string match (always safe)
1017
- if (filePath.includes(pattern)) {
1018
- return true;
1019
- }
1020
- // Then try regex match with error handling
1021
- try {
1022
- return new RegExp(pattern).test(filePath);
1023
- }
1024
- catch {
1025
- // Invalid regex pattern - already checked via includes() above
1026
- return false;
1445
+ getClassNameForNode(node) {
1446
+ // Use the parentClassName that was tracked during parseChildNodes
1447
+ return node.properties.parentClassName;
1448
+ }
1449
+ /**
1450
+ * Find the parent class node for a method/property node.
1451
+ * Uses the parentClassName property tracked during parsing.
1452
+ */
1453
+ findParentClassNode(node) {
1454
+ const parentClassName = node.properties.parentClassName;
1455
+ if (!parentClassName)
1456
+ return undefined;
1457
+ // Find the class node by name and file path
1458
+ for (const [, classNode] of this.parsedNodes) {
1459
+ if (classNode.coreType === CoreNodeType.CLASS_DECLARATION &&
1460
+ classNode.properties.name === parentClassName &&
1461
+ classNode.properties.filePath === node.properties.filePath) {
1462
+ return classNode;
1463
+ }
1027
1464
  }
1465
+ return undefined;
1028
1466
  }
1029
1467
  shouldSkipFile(sourceFile) {
1030
1468
  const filePath = sourceFile.getFilePath();
1031
1469
  const excludedPatterns = this.parseConfig.excludePatterns ?? [];
1032
1470
  for (const pattern of excludedPatterns) {
1033
- if (this.matchesPattern(filePath, pattern)) {
1471
+ if (matchesPattern(filePath, pattern)) {
1034
1472
  return true;
1035
1473
  }
1036
1474
  }
1037
1475
  return false;
1038
1476
  }
1039
- toNeo4jNode(parsedNode) {
1040
- return {
1041
- id: parsedNode.id,
1042
- labels: parsedNode.labels,
1043
- properties: parsedNode.properties,
1044
- skipEmbedding: parsedNode.skipEmbedding ?? false,
1045
- };
1046
- }
1047
- toNeo4jEdge(parsedEdge) {
1048
- return {
1049
- id: parsedEdge.id,
1050
- type: parsedEdge.relationshipType,
1051
- startNodeId: parsedEdge.sourceNodeId,
1052
- endNodeId: parsedEdge.targetNodeId,
1053
- properties: parsedEdge.properties,
1054
- };
1055
- }
1056
- addNode(node) {
1057
- this.parsedNodes.set(node.id, node);
1477
+ shouldSkipChildNode(node) {
1478
+ const excludedNodeTypes = this.parseConfig.excludedNodeTypes ?? [];
1479
+ return excludedNodeTypes.includes(node.getKindName());
1058
1480
  }
1059
- addEdge(edge) {
1060
- this.parsedEdges.set(edge.id, edge);
1481
+ /**
1482
+ * Check if variable declarations should be parsed for this file
1483
+ * based on framework schema configurations
1484
+ */
1485
+ shouldParseVariables(filePath) {
1486
+ for (const schema of this.frameworkSchemas) {
1487
+ const parsePatterns = schema.metadata.parseVariablesFrom;
1488
+ if (parsePatterns) {
1489
+ for (const pattern of parsePatterns) {
1490
+ if (minimatch(filePath, pattern)) {
1491
+ return true;
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+ return false;
1061
1497
  }
1062
- // Helper methods for statistics and debugging
1063
1498
  getStats() {
1064
1499
  const nodesByType = {};
1065
1500
  const nodesBySemanticType = {};
@@ -1092,17 +1527,24 @@ export class TypeScriptParser {
1092
1527
  }));
1093
1528
  return { nodes, edges };
1094
1529
  }
1095
- // ============================================
1096
- // CHUNK-AWARE PARSING METHODS
1097
- // For streaming/chunked parsing of large codebases
1098
- // ============================================
1530
+ /**
1531
+ * Get count of currently parsed nodes and edges.
1532
+ * Useful for progress reporting.
1533
+ */
1534
+ getCurrentCounts() {
1535
+ return {
1536
+ nodes: this.parsedNodes.size,
1537
+ edges: this.parsedEdges.size,
1538
+ deferredEdges: this.deferredEdges.length,
1539
+ };
1540
+ }
1099
1541
  /**
1100
1542
  * Export current chunk results without clearing internal state.
1101
1543
  * Use this when importing chunks incrementally.
1102
1544
  */
1103
1545
  exportChunkResults() {
1104
- const nodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
1105
- const edges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
1546
+ const nodes = Array.from(this.parsedNodes.values()).map(toNeo4jNode);
1547
+ const edges = Array.from(this.parsedEdges.values()).map(toNeo4jEdge);
1106
1548
  return {
1107
1549
  nodes,
1108
1550
  edges,
@@ -1117,259 +1559,114 @@ export class TypeScriptParser {
1117
1559
  this.parsedNodes.clear();
1118
1560
  this.parsedEdges.clear();
1119
1561
  this.deferredEdges = [];
1562
+ // Clear CALLS edge lookup indexes
1563
+ this.methodsByClass.clear();
1564
+ this.functionsByName.clear();
1565
+ this.constructorsByClass.clear();
1566
+ this.exportedNodeIds.clear();
1567
+ this.exportedEdgeIds.clear();
1120
1568
  }
1121
1569
  /**
1122
- * Get count of currently parsed nodes and edges.
1123
- * Useful for progress reporting.
1124
- */
1125
- getCurrentCounts() {
1126
- return {
1127
- nodes: this.parsedNodes.size,
1128
- edges: this.parsedEdges.size,
1129
- deferredEdges: this.deferredEdges.length,
1130
- };
1131
- }
1132
- /**
1133
- * Set the shared context for this parser.
1134
- * Use this to share context across multiple parsers (e.g., in WorkspaceParser).
1135
- * @param context The shared context map to use
1136
- */
1137
- setSharedContext(context) {
1138
- this.sharedContext = context;
1139
- }
1140
- /**
1141
- * Get the shared context from this parser.
1142
- * Useful for aggregating context across multiple parsers.
1143
- */
1144
- getSharedContext() {
1145
- return this.sharedContext;
1146
- }
1147
- /**
1148
- * Get all parsed nodes (for cross-parser edge resolution).
1149
- * Returns the internal Map of ParsedNodes.
1150
- */
1151
- getParsedNodes() {
1152
- return this.parsedNodes;
1153
- }
1154
- /**
1155
- * Get the framework schemas used by this parser.
1156
- * Useful for WorkspaceParser to apply cross-package edge enhancements.
1157
- */
1158
- getFrameworkSchemas() {
1159
- return this.frameworkSchemas;
1160
- }
1161
- /**
1162
- * Defer edge enhancements to a parent parser (e.g., WorkspaceParser).
1163
- * When true, parseChunk() will skip applyEdgeEnhancements().
1164
- * The parent is responsible for calling applyEdgeEnhancementsManually() at the end.
1165
- */
1166
- setDeferEdgeEnhancements(defer) {
1167
- this.deferEdgeEnhancements = defer;
1168
- }
1169
- /**
1170
- * Get list of source files in the project.
1171
- * In lazy mode, uses glob to discover files without loading them into memory.
1172
- * Useful for determining total work and creating chunks.
1570
+ * Add nodes to the existing nodes map for cross-chunk edge resolution.
1571
+ * These nodes are considered as potential edge targets but won't be exported.
1173
1572
  */
1174
- async discoverSourceFiles() {
1175
- if (this.discoveredFiles !== null) {
1176
- return this.discoveredFiles;
1177
- }
1178
- if (this.lazyLoad) {
1179
- // Use glob to find files without loading them into ts-morph
1180
- // Include both .ts and .tsx files
1181
- const pattern = path.join(this.workspacePath, '**/*.{ts,tsx}');
1182
- const allFiles = await glob(pattern, {
1183
- ignore: ['**/node_modules/**', '**/*.d.ts'],
1184
- absolute: true,
1185
- });
1186
- // Apply exclude patterns from parseConfig
1187
- const excludedPatterns = this.parseConfig.excludePatterns ?? [];
1188
- this.discoveredFiles = allFiles.filter((filePath) => {
1189
- for (const excludePattern of excludedPatterns) {
1190
- if (this.matchesPattern(filePath, excludePattern)) {
1191
- return false;
1192
- }
1193
- }
1194
- return true;
1195
- });
1196
- console.log(`🔍 Discovered ${this.discoveredFiles.length} TypeScript files (lazy mode)`);
1197
- return this.discoveredFiles;
1198
- }
1199
- else {
1200
- // Eager mode - files are already loaded
1201
- this.discoveredFiles = this.project
1202
- .getSourceFiles()
1203
- .filter((sf) => !this.shouldSkipFile(sf))
1204
- .map((sf) => sf.getFilePath());
1205
- return this.discoveredFiles;
1573
+ addExistingNodesFromChunk(nodes) {
1574
+ for (const node of nodes) {
1575
+ const parsedNode = {
1576
+ id: node.id,
1577
+ coreType: node.properties.coreType,
1578
+ semanticType: node.properties.semanticType,
1579
+ labels: node.labels,
1580
+ properties: node.properties,
1581
+ };
1582
+ this.existingNodes.set(node.id, parsedNode);
1583
+ this.indexNodeForCallsResolution(parsedNode);
1206
1584
  }
1207
1585
  }
1208
1586
  /**
1209
- * @deprecated Use discoverSourceFiles() instead for async file discovery
1587
+ * Add nodes to parsedNodes for edge enhancement.
1588
+ * Use this when coordinator needs to run edge enhancements on accumulated chunk results.
1589
+ * Unlike addExistingNodesFromChunk, these nodes ARE used as edge sources.
1210
1590
  */
1211
- getSourceFilePaths() {
1212
- if (this.lazyLoad) {
1213
- throw new Error('getSourceFilePaths() is not supported in lazy mode. Use discoverSourceFiles() instead.');
1591
+ addParsedNodesFromChunk(nodes) {
1592
+ for (const node of nodes) {
1593
+ const parsedNode = {
1594
+ id: node.id,
1595
+ coreType: node.properties.coreType,
1596
+ semanticType: node.properties.semanticType,
1597
+ labels: node.labels,
1598
+ properties: node.properties,
1599
+ };
1600
+ this.parsedNodes.set(node.id, parsedNode);
1601
+ this.indexNodeForCallsResolution(parsedNode);
1214
1602
  }
1215
- return this.project
1216
- .getSourceFiles()
1217
- .filter((sf) => !this.shouldSkipFile(sf))
1218
- .map((sf) => sf.getFilePath());
1219
1603
  }
1220
1604
  /**
1221
- * Parse a chunk of files without resolving deferred edges.
1222
- * Use this for streaming parsing where edges are resolved after all chunks.
1223
- * In lazy mode, files are added to the project just-in-time and removed after parsing.
1224
- * @param filePaths Specific file paths to parse
1225
- * @param skipEdgeResolution If true, deferred edges are not resolved (default: false)
1605
+ * Get serialized shared context for parallel parsing.
1606
+ * Converts Maps to arrays for structured clone compatibility.
1226
1607
  */
1227
- async parseChunk(filePaths, skipEdgeResolution = false) {
1228
- // Declare sourceFiles outside try so it's available in finally
1229
- const sourceFiles = [];
1230
- try {
1231
- if (this.lazyLoad) {
1232
- // Lazy mode: add files to project just-in-time
1233
- for (const filePath of filePaths) {
1234
- try {
1235
- // Check if file already exists in project (shouldn't happen in lazy mode)
1236
- // Add the file to the project if not already present
1237
- const sourceFile = this.project.getSourceFile(filePath) ?? this.project.addSourceFileAtPath(filePath);
1238
- sourceFiles.push(sourceFile);
1239
- }
1240
- catch (error) {
1241
- console.warn(`Failed to add source file ${filePath}:`, error);
1242
- }
1243
- }
1608
+ getSerializedSharedContext() {
1609
+ const serialized = [];
1610
+ for (const [key, value] of this.sharedContext) {
1611
+ // Convert nested Maps to arrays
1612
+ if (value instanceof Map) {
1613
+ serialized.push([key, Array.from(value.entries())]);
1244
1614
  }
1245
1615
  else {
1246
- // Eager mode: files are already loaded
1247
- const loadedFiles = filePaths
1248
- .map((filePath) => this.project.getSourceFile(filePath))
1249
- .filter((sf) => sf !== undefined);
1250
- sourceFiles.push(...loadedFiles);
1251
- }
1252
- for (const sourceFile of sourceFiles) {
1253
- if (this.shouldSkipFile(sourceFile))
1254
- continue;
1255
- await this.parseCoreTypeScriptV2(sourceFile);
1256
- }
1257
- // Only resolve edges if not skipping
1258
- if (!skipEdgeResolution) {
1259
- await this.resolveDeferredEdges();
1260
- }
1261
- await this.applyContextExtractors();
1262
- if (this.frameworkSchemas.length > 0) {
1263
- await this.applyFrameworkEnhancements();
1264
- }
1265
- // Apply edge enhancements unless deferred to parent (e.g., WorkspaceParser)
1266
- // When deferred, parent will call applyEdgeEnhancementsManually() at the end
1267
- // with all accumulated nodes for cross-package edge detection
1268
- if (!this.deferEdgeEnhancements) {
1269
- await this.applyEdgeEnhancements();
1270
- }
1271
- const neo4jNodes = Array.from(this.parsedNodes.values()).map(this.toNeo4jNode);
1272
- const neo4jEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
1273
- return { nodes: neo4jNodes, edges: neo4jEdges };
1274
- }
1275
- finally {
1276
- // Always clean up in lazy mode to prevent memory leaks
1277
- if (this.lazyLoad) {
1278
- for (const sourceFile of sourceFiles) {
1279
- try {
1280
- this.project.removeSourceFile(sourceFile);
1281
- }
1282
- catch {
1283
- // Ignore errors when removing files
1284
- }
1285
- }
1616
+ serialized.push([key, value]);
1286
1617
  }
1287
1618
  }
1619
+ return serialized;
1288
1620
  }
1289
1621
  /**
1290
- * Resolve deferred edges against both parsed nodes and existing nodes.
1291
- * Call this after all chunks have been parsed.
1292
- * @returns Resolved edges
1622
+ * Merge serialized shared context from workers.
1623
+ * Handles Map merging by combining entries.
1293
1624
  */
1294
- async resolveDeferredEdgesManually() {
1295
- const resolvedEdges = [];
1296
- // Count edges by type for logging
1297
- const extendsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.EXTENDS).length;
1298
- const implementsCount = this.deferredEdges.filter((e) => e.edgeType === CoreEdgeType.IMPLEMENTS).length;
1299
- let extendsResolved = 0;
1300
- let implementsResolved = 0;
1301
- const unresolvedExtends = [];
1302
- const unresolvedImplements = [];
1303
- for (const deferred of this.deferredEdges) {
1304
- // Pass filePath for precise matching (especially important for EXTENDS/IMPLEMENTS)
1305
- const targetNode = this.findNodeByNameAndType(deferred.targetName, deferred.targetType, deferred.targetFilePath);
1306
- if (targetNode) {
1307
- const edge = this.createCoreEdge(deferred.edgeType, deferred.sourceNodeId, targetNode.id);
1308
- resolvedEdges.push(edge);
1309
- this.addEdge(edge);
1310
- if (deferred.edgeType === CoreEdgeType.EXTENDS) {
1311
- extendsResolved++;
1312
- }
1313
- else if (deferred.edgeType === CoreEdgeType.IMPLEMENTS) {
1314
- implementsResolved++;
1625
+ mergeSerializedSharedContext(serialized) {
1626
+ for (const [key, value] of serialized) {
1627
+ if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
1628
+ // It's a serialized Map - merge with existing
1629
+ const existingMap = this.sharedContext.get(key);
1630
+ const newMap = existingMap ?? new Map();
1631
+ for (const [k, v] of value) {
1632
+ newMap.set(k, v);
1315
1633
  }
1634
+ this.sharedContext.set(key, newMap);
1316
1635
  }
1317
1636
  else {
1318
- if (deferred.edgeType === CoreEdgeType.EXTENDS) {
1319
- unresolvedExtends.push(deferred.targetName);
1320
- }
1321
- else if (deferred.edgeType === CoreEdgeType.IMPLEMENTS) {
1322
- unresolvedImplements.push(deferred.targetName);
1323
- }
1637
+ // Simple value - just set it
1638
+ this.sharedContext.set(key, value);
1324
1639
  }
1325
1640
  }
1326
- // Log inheritance resolution stats
1327
- if (extendsCount > 0 || implementsCount > 0) {
1328
- await debugLog('Inheritance edge resolution (manual)', {
1329
- extendsQueued: extendsCount,
1330
- extendsResolved,
1331
- extendsUnresolved: unresolvedExtends.length,
1332
- unresolvedExtendsSample: unresolvedExtends.slice(0, 10),
1333
- implementsQueued: implementsCount,
1334
- implementsResolved,
1335
- implementsUnresolved: unresolvedImplements.length,
1336
- unresolvedImplementsSample: unresolvedImplements.slice(0, 10),
1337
- });
1338
- }
1339
- this.deferredEdges = [];
1340
- return resolvedEdges.map(this.toNeo4jEdge);
1341
1641
  }
1342
1642
  /**
1343
- * Apply edge enhancements on all accumulated nodes.
1344
- * Call this after all chunks have been parsed for streaming mode.
1345
- * This allows context-dependent edges (like INTERNAL_API_CALL) to be detected
1346
- * after all nodes and their context have been collected.
1347
- * @returns New edges created by edge enhancements
1643
+ * Get deferred edges for cross-chunk resolution.
1644
+ * Returns serializable format for worker thread transfer.
1348
1645
  */
1349
- async applyEdgeEnhancementsManually() {
1350
- const edgeCountBefore = this.parsedEdges.size;
1351
- console.log(`🔗 Applying edge enhancements on ${this.parsedNodes.size} accumulated nodes...`);
1352
- await this.applyEdgeEnhancements();
1353
- const newEdgeCount = this.parsedEdges.size - edgeCountBefore;
1354
- console.log(` ✅ Created ${newEdgeCount} edges from edge enhancements`);
1355
- // Return only the new edges (those created by edge enhancements)
1356
- const allEdges = Array.from(this.parsedEdges.values()).map(this.toNeo4jEdge);
1357
- return allEdges.slice(edgeCountBefore);
1646
+ getDeferredEdges() {
1647
+ return this.deferredEdges.map((e) => ({
1648
+ edgeType: e.edgeType,
1649
+ sourceNodeId: e.sourceNodeId,
1650
+ targetName: e.targetName,
1651
+ targetType: e.targetType,
1652
+ targetFilePath: e.targetFilePath,
1653
+ callContext: e.callContext,
1654
+ }));
1358
1655
  }
1359
1656
  /**
1360
- * Add nodes to the existing nodes map for cross-chunk edge resolution.
1361
- * These nodes are considered as potential edge targets but won't be exported.
1657
+ * Merge deferred edges from workers for resolution.
1658
+ * Converts back to internal format with CoreEdgeType/CoreNodeType.
1362
1659
  */
1363
- addExistingNodesFromChunk(nodes) {
1364
- for (const node of nodes) {
1365
- const parsedNode = {
1366
- id: node.id,
1367
- coreType: node.properties.coreType,
1368
- semanticType: node.properties.semanticType,
1369
- labels: node.labels,
1370
- properties: node.properties,
1371
- };
1372
- this.existingNodes.set(node.id, parsedNode);
1660
+ mergeDeferredEdges(edges) {
1661
+ for (const e of edges) {
1662
+ this.deferredEdges.push({
1663
+ edgeType: e.edgeType,
1664
+ sourceNodeId: e.sourceNodeId,
1665
+ targetName: e.targetName,
1666
+ targetType: e.targetType,
1667
+ targetFilePath: e.targetFilePath,
1668
+ callContext: e.callContext,
1669
+ });
1373
1670
  }
1374
1671
  }
1375
1672
  }