gitnexus 1.4.7 → 1.4.8

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 (92) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +2 -1
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +48 -1
  24. package/dist/core/ingestion/call-processor.js +368 -7
  25. package/dist/core/ingestion/call-routing.d.ts +6 -0
  26. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  27. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  28. package/dist/core/ingestion/framework-detection.js +49 -12
  29. package/dist/core/ingestion/heritage-processor.js +47 -49
  30. package/dist/core/ingestion/import-processor.d.ts +1 -1
  31. package/dist/core/ingestion/import-processor.js +103 -194
  32. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  33. package/dist/core/ingestion/import-resolution.js +251 -0
  34. package/dist/core/ingestion/language-config.d.ts +3 -0
  35. package/dist/core/ingestion/language-config.js +13 -0
  36. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  37. package/dist/core/ingestion/markdown-processor.js +124 -0
  38. package/dist/core/ingestion/mro-processor.js +8 -3
  39. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  40. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  41. package/dist/core/ingestion/parsing-processor.d.ts +2 -2
  42. package/dist/core/ingestion/parsing-processor.js +14 -73
  43. package/dist/core/ingestion/pipeline.d.ts +10 -0
  44. package/dist/core/ingestion/pipeline.js +421 -4
  45. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  46. package/dist/core/ingestion/resolution-context.js +7 -4
  47. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  48. package/dist/core/ingestion/resolvers/index.js +1 -1
  49. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  50. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  51. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  52. package/dist/core/ingestion/resolvers/php.js +43 -3
  53. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  54. package/dist/core/ingestion/resolvers/utils.js +16 -0
  55. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  56. package/dist/core/ingestion/symbol-table.js +20 -6
  57. package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
  58. package/dist/core/ingestion/tree-sitter-queries.js +43 -2
  59. package/dist/core/ingestion/type-env.d.ts +28 -1
  60. package/dist/core/ingestion/type-env.js +419 -96
  61. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  62. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  63. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  64. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  65. package/dist/core/ingestion/type-extractors/index.js +1 -1
  66. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  67. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  68. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
  69. package/dist/core/ingestion/type-extractors/shared.js +5 -10
  70. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  71. package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
  72. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  73. package/dist/core/ingestion/utils.d.ts +2 -120
  74. package/dist/core/ingestion/utils.js +3 -1051
  75. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
  76. package/dist/core/ingestion/workers/parse-worker.js +66 -87
  77. package/dist/core/lbug/csv-generator.js +18 -1
  78. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  79. package/dist/core/lbug/lbug-adapter.js +69 -4
  80. package/dist/core/lbug/schema.d.ts +5 -3
  81. package/dist/core/lbug/schema.js +26 -2
  82. package/dist/mcp/core/embedder.js +11 -3
  83. package/dist/mcp/core/lbug-adapter.js +12 -1
  84. package/dist/mcp/local/local-backend.d.ts +22 -0
  85. package/dist/mcp/local/local-backend.js +133 -29
  86. package/dist/mcp/resources.js +2 -0
  87. package/dist/mcp/tools.js +2 -2
  88. package/dist/server/api.d.ts +19 -1
  89. package/dist/server/api.js +66 -6
  90. package/dist/storage/git.d.ts +12 -0
  91. package/dist/storage/git.js +21 -0
  92. package/package.json +10 -2
@@ -4,10 +4,136 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
4
4
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
5
5
  import { generateId } from '../../lib/utils.js';
6
6
  import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, extractMixedChain, } from './utils.js';
7
- import { buildTypeEnv } from './type-env.js';
7
+ import { buildTypeEnv, isSubclassOf } from './type-env.js';
8
8
  import { getTreeSitterBufferSize } from './constants.js';
9
9
  import { callRouters } from './call-routing.js';
10
10
  import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
11
+ import { typeConfigs } from './type-extractors/index.js';
12
+ const MAX_EXPORTS_PER_FILE = 500;
13
+ const MAX_TYPE_NAME_LENGTH = 256;
14
+ /** Build a map of imported callee names → return types for cross-file call-result binding.
15
+ * Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle). */
16
+ export function buildImportedReturnTypes(filePath, namedImportMap, symbolTable) {
17
+ const result = new Map();
18
+ const fileImports = namedImportMap.get(filePath);
19
+ if (!fileImports)
20
+ return result;
21
+ for (const [localName, binding] of fileImports) {
22
+ const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
23
+ if (!def?.returnType)
24
+ continue;
25
+ const simpleReturn = extractReturnTypeName(def.returnType);
26
+ if (simpleReturn)
27
+ result.set(localName, simpleReturn);
28
+ }
29
+ return result;
30
+ }
31
+ /** Build cross-file RAW return types for imported callables.
32
+ * Unlike buildImportedReturnTypes (which stores extractReturnTypeName output),
33
+ * this stores the raw declared return type string (e.g., 'User[]', 'List<User>').
34
+ * Used by lookupRawReturnType for for-loop element extraction via extractElementTypeFromString. */
35
+ export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTable) {
36
+ const result = new Map();
37
+ const fileImports = namedImportMap.get(filePath);
38
+ if (!fileImports)
39
+ return result;
40
+ for (const [localName, binding] of fileImports) {
41
+ const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName);
42
+ if (!def?.returnType)
43
+ continue;
44
+ result.set(localName, def.returnType);
45
+ }
46
+ return result;
47
+ }
48
+ /** Collect resolved type bindings for exported file-scope symbols.
49
+ * Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
50
+ function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
51
+ const fileScope = typeEnv.env.get('');
52
+ if (!fileScope || fileScope.size === 0)
53
+ return null;
54
+ const exported = new Map();
55
+ for (const [varName, typeName] of fileScope) {
56
+ if (exported.size >= MAX_EXPORTS_PER_FILE)
57
+ break;
58
+ if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
59
+ continue;
60
+ const nodeId = symbolTable.lookupExact(filePath, varName);
61
+ if (!nodeId)
62
+ continue;
63
+ const node = graph.getNode(nodeId);
64
+ if (node?.properties?.isExported) {
65
+ exported.set(varName, typeName);
66
+ }
67
+ }
68
+ return exported.size > 0 ? exported : null;
69
+ }
70
+ /** Build ExportedTypeMap from graph nodes — used for worker path where TypeEnv
71
+ * is not available in the main thread. Collects returnType/declaredType from
72
+ * exported symbols that have callables with known return types. */
73
+ export function buildExportedTypeMapFromGraph(graph, symbolTable) {
74
+ const result = new Map();
75
+ graph.forEachNode(node => {
76
+ if (!node.properties?.isExported)
77
+ return;
78
+ if (!node.properties?.filePath || !node.properties?.name)
79
+ return;
80
+ const filePath = node.properties.filePath;
81
+ const name = node.properties.name;
82
+ if (!name || name.length > MAX_TYPE_NAME_LENGTH)
83
+ return;
84
+ // For callable symbols, use returnType; for properties/variables, use declaredType
85
+ const def = symbolTable.lookupExactFull(filePath, name);
86
+ if (!def)
87
+ return;
88
+ const typeName = def.returnType ?? def.declaredType;
89
+ if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH)
90
+ return;
91
+ // Extract simple type name (strip Promise<>, etc.) — reuse shared utility
92
+ const simpleType = extractReturnTypeName(typeName) ?? typeName;
93
+ if (!simpleType)
94
+ return;
95
+ let fileExports = result.get(filePath);
96
+ if (!fileExports) {
97
+ fileExports = new Map();
98
+ result.set(filePath, fileExports);
99
+ }
100
+ if (fileExports.size < MAX_EXPORTS_PER_FILE) {
101
+ fileExports.set(name, simpleType);
102
+ }
103
+ });
104
+ return result;
105
+ }
106
+ /** Seed cross-file receiver types into pre-extracted call records.
107
+ * Fills missing receiverTypeName for single-hop imported variables
108
+ * using ExportedTypeMap + namedImportMap — zero disk I/O, zero AST re-parsing.
109
+ * Mutates calls in-place. Runs BEFORE processCallsFromExtracted. */
110
+ export function seedCrossFileReceiverTypes(calls, namedImportMap, exportedTypeMap) {
111
+ if (namedImportMap.size === 0 || exportedTypeMap.size === 0) {
112
+ return { enrichedCount: 0 };
113
+ }
114
+ let enrichedCount = 0;
115
+ for (const call of calls) {
116
+ if (call.receiverTypeName || !call.receiverName)
117
+ continue;
118
+ if (call.callForm !== 'member')
119
+ continue;
120
+ const fileImports = namedImportMap.get(call.filePath);
121
+ if (!fileImports)
122
+ continue;
123
+ const binding = fileImports.get(call.receiverName);
124
+ if (!binding)
125
+ continue;
126
+ const upstream = exportedTypeMap.get(binding.sourcePath);
127
+ if (!upstream)
128
+ continue;
129
+ const type = upstream.get(binding.exportedName);
130
+ if (type) {
131
+ call.receiverTypeName = type;
132
+ enrichedCount++;
133
+ }
134
+ }
135
+ return { enrichedCount };
136
+ }
11
137
  // Stdlib methods that preserve the receiver's type identity. When TypeEnv already
12
138
  // strips nullable wrappers (Option<User> → User), these chain steps are no-ops
13
139
  // for type resolution — the current type passes through unchanged.
@@ -87,10 +213,23 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
87
213
  }
88
214
  return verified;
89
215
  };
90
- export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
216
+ export const processCalls = async (graph, files, astCache, ctx, onProgress, exportedTypeMap,
217
+ /** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map<localName, typeName>. */
218
+ importedBindingsMap,
219
+ /** Phase 14 E3: cross-file return types for imported callables. Keyed by filePath → Map<calleeName, returnType>.
220
+ * Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */
221
+ importedReturnTypesMap,
222
+ /** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map<calleeName, rawReturnType>. */
223
+ importedRawReturnTypesMap) => {
91
224
  const parser = await loadParser();
92
225
  const collectedHeritage = [];
93
226
  const pendingWrites = [];
227
+ // Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf.
228
+ // Used as a secondary check when per-file parentMap lacks the relationship — helps
229
+ // when the heritage-declaring file is processed before the call site file.
230
+ // For remaining cases (reverse file order), the SymbolTable class-type fallback applies.
231
+ const globalParentMap = new Map();
232
+ const globalParentSeen = new Map();
94
233
  const logSkipped = isVerboseIngestionEnabled();
95
234
  const skippedByLang = logSkipped ? new Map() : null;
96
235
  for (let i = 0; i < files.length; i++) {
@@ -133,7 +272,58 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
133
272
  continue;
134
273
  }
135
274
  const lang = getLanguageFromFilename(file.path);
136
- const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
275
+ // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
276
+ // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
277
+ const fileParentMap = new Map();
278
+ for (const match of matches) {
279
+ const captureMap = {};
280
+ match.captures.forEach(c => captureMap[c.name] = c.node);
281
+ if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
282
+ const className = captureMap['heritage.class'].text;
283
+ const parentName = captureMap['heritage.extends'].text;
284
+ const extendsNode = captureMap['heritage.extends'];
285
+ const fieldDecl = extendsNode.parent;
286
+ if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
287
+ continue;
288
+ let parents = fileParentMap.get(className);
289
+ if (!parents) {
290
+ parents = [];
291
+ fileParentMap.set(className, parents);
292
+ }
293
+ if (!parents.includes(parentName))
294
+ parents.push(parentName);
295
+ }
296
+ }
297
+ const parentMap = fileParentMap;
298
+ // Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
299
+ // Uses a parallel Set (globalParentSeen) for O(1) deduplication instead of O(n) includes().
300
+ for (const [cls, parents] of fileParentMap) {
301
+ let global = globalParentMap.get(cls);
302
+ let seen = globalParentSeen.get(cls);
303
+ if (!global) {
304
+ global = [];
305
+ globalParentMap.set(cls, global);
306
+ }
307
+ if (!seen) {
308
+ seen = new Set();
309
+ globalParentSeen.set(cls, seen);
310
+ }
311
+ for (const p of parents) {
312
+ if (!seen.has(p)) {
313
+ seen.add(p);
314
+ global.push(p);
315
+ }
316
+ }
317
+ }
318
+ const importedBindings = importedBindingsMap?.get(file.path);
319
+ const importedReturnTypes = importedReturnTypesMap?.get(file.path);
320
+ const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
321
+ const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes }) : null;
322
+ if (typeEnv && exportedTypeMap) {
323
+ const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
324
+ if (fileExports)
325
+ exportedTypeMap.set(file.path, fileExports);
326
+ }
137
327
  const callRouter = callRouters[language];
138
328
  const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
139
329
  ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
@@ -245,6 +435,44 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
245
435
  const callForm = inferCallForm(callNode, nameNode);
246
436
  const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
247
437
  let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
438
+ // Phase P: virtual dispatch override — when the declared type is a base class but
439
+ // the constructor created a known subclass, prefer the more specific type.
440
+ // Checks per-file parentMap first, then falls back to globalParentMap for
441
+ // cross-file heritage (e.g. Dog extends Animal declared in a different file).
442
+ // Reconstructs the exact scope key (funcName@startIndex\0varName) from the
443
+ // enclosing function AST node for a correct, O(1) map lookup.
444
+ if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) {
445
+ // Reconstruct scope key to match constructorTypeMap's scope\0varName format
446
+ let scope = '';
447
+ let p = callNode.parent;
448
+ while (p) {
449
+ if (FUNCTION_NODE_TYPES.has(p.type)) {
450
+ const { funcName } = extractFunctionName(p);
451
+ if (funcName) {
452
+ scope = `${funcName}@${p.startIndex}`;
453
+ break;
454
+ }
455
+ }
456
+ p = p.parent;
457
+ }
458
+ const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`);
459
+ if (ctorType && ctorType !== receiverTypeName) {
460
+ // Verify subclass relationship: per-file parentMap first, then cross-file
461
+ // globalParentMap, then fall back to SymbolTable class verification.
462
+ // The SymbolTable fallback handles cross-file cases where heritage is declared
463
+ // in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when
464
+ // processing services/App.kt). Since constructorTypeMap only records entries
465
+ // when a type annotation AND constructor are both present (val x: Base = Sub()),
466
+ // confirming both are class-like types is sufficient — the original code would
467
+ // not compile if Sub didn't extend Base.
468
+ if (isSubclassOf(ctorType, receiverTypeName, parentMap)
469
+ || isSubclassOf(ctorType, receiverTypeName, globalParentMap)
470
+ || (ctx.symbols.lookupFuzzy(ctorType).some(d => d.type === 'Class' || d.type === 'Struct')
471
+ && ctx.symbols.lookupFuzzy(receiverTypeName).some(d => d.type === 'Class' || d.type === 'Struct' || d.type === 'Interface'))) {
472
+ receiverTypeName = ctorType;
473
+ }
474
+ }
475
+ }
248
476
  // Fall back to verified constructor bindings for return type inference
249
477
  if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
250
478
  const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
@@ -290,12 +518,19 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
290
518
  }
291
519
  }
292
520
  }
521
+ // Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
522
+ // Only used when multiple candidates survive arity filtering — ~1-3% of calls.
523
+ const langConfig = lang ? typeConfigs[lang] : undefined;
524
+ const hints = langConfig?.inferLiteralType
525
+ ? { callNode, inferLiteralType: langConfig.inferLiteralType }
526
+ : undefined;
293
527
  const resolved = resolveCallTarget({
294
528
  calledName,
295
529
  argCount: countCallArguments(callNode),
296
530
  callForm,
297
531
  receiverTypeName,
298
- }, file.path, ctx);
532
+ receiverName,
533
+ }, file.path, ctx, hints);
299
534
  if (!resolved)
300
535
  return;
301
536
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
@@ -362,7 +597,9 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
362
597
  const hasParameterMetadata = kindFiltered.some(candidate => candidate.parameterCount !== undefined);
363
598
  if (!hasParameterMetadata)
364
599
  return kindFiltered;
365
- return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
600
+ return kindFiltered.filter(candidate => candidate.parameterCount === undefined
601
+ || (argCount >= (candidate.requiredParameterCount ?? candidate.parameterCount)
602
+ && argCount <= candidate.parameterCount));
366
603
  };
367
604
  const toResolveResult = (definition, tier) => ({
368
605
  nodeId: definition.nodeId,
@@ -370,20 +607,129 @@ const toResolveResult = (definition, tier) => ({
370
607
  reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
371
608
  returnType: definition.returnType,
372
609
  });
610
+ /**
611
+ * Kotlin (and JVM in general) uses boxed type names in parameter declarations
612
+ * (e.g. `Int`, `Long`, `Boolean`) while inferJvmLiteralType returns unboxed
613
+ * primitives (`int`, `long`, `boolean`). Normalise both sides to lowercase so
614
+ * that the comparison `'Int' === 'int'` does not fail.
615
+ *
616
+ * Only applied to single-word identifiers that look like a JVM primitive alias;
617
+ * multi-word or qualified names are left untouched.
618
+ */
619
+ const KOTLIN_BOXED_TO_PRIMITIVE = {
620
+ Int: 'int',
621
+ Long: 'long',
622
+ Short: 'short',
623
+ Byte: 'byte',
624
+ Float: 'float',
625
+ Double: 'double',
626
+ Boolean: 'boolean',
627
+ Char: 'char',
628
+ };
629
+ const normalizeJvmTypeName = (name) => KOTLIN_BOXED_TO_PRIMITIVE[name] ?? name;
630
+ /**
631
+ * Try to disambiguate overloaded candidates using argument literal types.
632
+ * Only invoked when filteredCandidates.length > 1 and at least one has parameterTypes.
633
+ * Returns the single matching candidate, or null if ambiguous/inconclusive.
634
+ */
635
+ const tryOverloadDisambiguation = (candidates, hints) => {
636
+ if (!candidates.some(c => c.parameterTypes))
637
+ return null;
638
+ // Find the argument list node in the call expression.
639
+ // Kotlin wraps value_arguments inside a call_suffix child, so we must also
640
+ // search one level deeper when a direct match is not found.
641
+ let argList = hints.callNode.childForFieldName?.('arguments')
642
+ ?? hints.callNode.children.find((c) => c.type === 'arguments' || c.type === 'argument_list' || c.type === 'value_arguments');
643
+ if (!argList) {
644
+ // Kotlin: call_expression → call_suffix → value_arguments
645
+ const callSuffix = hints.callNode.children.find((c) => c.type === 'call_suffix');
646
+ if (callSuffix) {
647
+ argList = callSuffix.children.find((c) => c.type === 'value_arguments');
648
+ }
649
+ }
650
+ if (!argList)
651
+ return null;
652
+ const argTypes = [];
653
+ for (const arg of argList.namedChildren) {
654
+ if (arg.type === 'comment')
655
+ continue;
656
+ // Unwrap argument wrapper nodes before passing to inferLiteralType:
657
+ // - Kotlin value_argument: has 'value' field containing the literal
658
+ // - C# argument: has 'expression' field (handles named args like `name: "alice"`
659
+ // where firstNamedChild would return name_colon instead of the value)
660
+ // - Java/others: arg IS the literal directly (no unwrapping needed)
661
+ const valueNode = arg.childForFieldName?.('value')
662
+ ?? arg.childForFieldName?.('expression')
663
+ ?? (arg.type === 'argument' || arg.type === 'value_argument'
664
+ ? arg.firstNamedChild ?? arg
665
+ : arg);
666
+ argTypes.push(hints.inferLiteralType(valueNode));
667
+ }
668
+ // If no literal types could be inferred, can't disambiguate
669
+ if (argTypes.every(t => t === undefined))
670
+ return null;
671
+ const matched = candidates.filter(c => {
672
+ // Keep candidates without type info — conservative: partially-annotated codebases
673
+ // (e.g. C++ with some missing declarations) may have mixed typed/untyped overloads.
674
+ // If one typed and one untyped both survive, matched.length > 1 → returns null (no edge).
675
+ if (!c.parameterTypes)
676
+ return true;
677
+ return c.parameterTypes.every((pType, i) => {
678
+ if (i >= argTypes.length || !argTypes[i])
679
+ return true;
680
+ // Normalise Kotlin boxed type names (Int→int, Boolean→boolean, etc.) so
681
+ // that the stored declaration type matches the inferred literal type.
682
+ return normalizeJvmTypeName(pType) === argTypes[i];
683
+ });
684
+ });
685
+ if (matched.length === 1)
686
+ return matched[0];
687
+ // Multiple survivors may share the same nodeId (e.g. TypeScript overload signatures +
688
+ // implementation body all collide via generateId). Deduplicate by nodeId — if all
689
+ // matched candidates resolve to the same graph node, disambiguation succeeded.
690
+ if (matched.length > 1) {
691
+ const uniqueIds = new Set(matched.map(c => c.nodeId));
692
+ if (uniqueIds.size === 1)
693
+ return matched[0];
694
+ }
695
+ return null;
696
+ };
373
697
  /**
374
698
  * Resolve a function call to its target node ID using priority strategy:
375
699
  * A. Narrow candidates by scope tier via ctx.resolve()
376
700
  * B. Filter to callable symbol kinds (constructor-aware when callForm is set)
377
701
  * C. Apply arity filtering when parameter metadata is available
378
702
  * D. Apply receiver-type filtering for member calls with typed receivers
703
+ * E. Apply overload disambiguation via argument literal types (when available)
379
704
  *
380
705
  * If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
381
706
  */
382
- const resolveCallTarget = (call, currentFile, ctx) => {
707
+ const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
383
708
  const tiered = ctx.resolve(call.calledName, currentFile);
384
709
  if (!tiered)
385
710
  return null;
386
- const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
711
+ let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
712
+ // Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
713
+ // The attribute access gives callForm='member', but the callee may be a Class — a valid
714
+ // constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
715
+ // emits a CALLS edge to the class node.
716
+ if (filteredCandidates.length === 0 && call.callForm === 'member') {
717
+ filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
718
+ }
719
+ // Module-alias disambiguation: Python `import auth; auth.User()` — when both models.py and
720
+ // auth.py export User, receiverName='auth' selects auth.py via moduleAliasMap.
721
+ // Runs when multiple candidates survive filtering and the receiver is a known module alias.
722
+ if (filteredCandidates.length > 1 && call.callForm === 'member' && call.receiverName) {
723
+ const aliasMap = ctx.moduleAliasMap?.get(currentFile);
724
+ if (aliasMap) {
725
+ const moduleFile = aliasMap.get(call.receiverName);
726
+ if (moduleFile) {
727
+ const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
728
+ if (aliasFiltered.length > 0)
729
+ filteredCandidates = aliasFiltered;
730
+ }
731
+ }
732
+ }
387
733
  // D. Receiver-type filtering: for member calls with a known receiver type,
388
734
  // resolve the type through the same tiered import infrastructure, then
389
735
  // filter method candidates to the type's defining file. Fall back to
@@ -415,10 +761,25 @@ const resolveCallTarget = (call, currentFile, ctx) => {
415
761
  if (ownerFiltered.length === 1) {
416
762
  return toResolveResult(ownerFiltered[0], tiered.tier);
417
763
  }
764
+ // E. Try overload disambiguation on the narrowed pool
765
+ if ((fileFiltered.length > 1 || ownerFiltered.length > 1) && overloadHints) {
766
+ const overloadPool = ownerFiltered.length > 1 ? ownerFiltered : fileFiltered;
767
+ const disambiguated = tryOverloadDisambiguation(overloadPool, overloadHints);
768
+ if (disambiguated)
769
+ return toResolveResult(disambiguated, tiered.tier);
770
+ }
418
771
  if (fileFiltered.length > 1 || ownerFiltered.length > 1)
419
772
  return null;
420
773
  }
421
774
  }
775
+ // E. Overload disambiguation: when multiple candidates survive arity + receiver filtering,
776
+ // try matching argument literal types against parameter types (Phase P).
777
+ // Only available on sequential path (has AST); worker path falls through gracefully.
778
+ if (filteredCandidates.length > 1 && overloadHints) {
779
+ const disambiguated = tryOverloadDisambiguation(filteredCandidates, overloadHints);
780
+ if (disambiguated)
781
+ return toResolveResult(disambiguated, tiered.tier);
782
+ }
422
783
  if (filteredCandidates.length !== 1)
423
784
  return null;
424
785
  return toResolveResult(filteredCandidates[0], tiered.tier);
@@ -12,6 +12,12 @@
12
12
  */
13
13
  /** null = this call was not routed; fall through to default call handling */
14
14
  export type CallRoutingResult = RubyCallRouting | null;
15
+ /**
16
+ * Per-language call router.
17
+ * IMPORTANT: Call-routed imports bypass preprocessImportPath(), so any router that
18
+ * returns an importPath MUST validate it independently (length cap, control-char
19
+ * rejection). See routeRubyCall for the reference implementation.
20
+ */
15
21
  export type CallRouter = (calledName: string, callNode: any) => CallRoutingResult;
16
22
  /** Per-language call routing. noRouting = no special routing (normal call processing) */
17
23
  export declare const callRouters: {
@@ -12,28 +12,31 @@
12
12
  import { detectFrameworkFromPath } from './framework-detection.js';
13
13
  import { SupportedLanguages } from '../../config/supported-languages.js';
14
14
  // ============================================================================
15
- // NAME PATTERNS - All 11 supported languages
15
+ // NAME PATTERNS - All 13 supported languages
16
16
  // ============================================================================
17
17
  /**
18
- * Common entry point naming patterns by language
19
- * These patterns indicate functions that are likely feature entry points
18
+ * Common entry point naming patterns by language.
19
+ * These patterns indicate functions that are likely feature entry points.
20
+ *
21
+ * Universal patterns are separated from per-language patterns so the per-language
22
+ * table can use `satisfies Record<SupportedLanguages, RegExp[]>` for compile-time
23
+ * exhaustiveness — the compiler catches any missing language entry.
20
24
  */
25
+ const UNIVERSAL_ENTRY_POINT_PATTERNS = [
26
+ /^(main|init|bootstrap|start|run|setup|configure)$/i,
27
+ /^handle[A-Z]/, // handleLogin, handleSubmit
28
+ /^on[A-Z]/, // onClick, onSubmit
29
+ /Handler$/, // RequestHandler
30
+ /Controller$/, // UserController
31
+ /^process[A-Z]/, // processPayment
32
+ /^execute[A-Z]/, // executeQuery
33
+ /^perform[A-Z]/, // performAction
34
+ /^dispatch[A-Z]/, // dispatchEvent
35
+ /^trigger[A-Z]/, // triggerAction
36
+ /^fire[A-Z]/, // fireEvent
37
+ /^emit[A-Z]/, // emitEvent
38
+ ];
21
39
  const ENTRY_POINT_PATTERNS = {
22
- // Universal patterns (apply to all languages)
23
- '*': [
24
- /^(main|init|bootstrap|start|run|setup|configure)$/i,
25
- /^handle[A-Z]/, // handleLogin, handleSubmit
26
- /^on[A-Z]/, // onClick, onSubmit
27
- /Handler$/, // RequestHandler
28
- /Controller$/, // UserController
29
- /^process[A-Z]/, // processPayment
30
- /^execute[A-Z]/, // executeQuery
31
- /^perform[A-Z]/, // performAction
32
- /^dispatch[A-Z]/, // dispatchEvent
33
- /^trigger[A-Z]/, // triggerAction
34
- /^fire[A-Z]/, // fireEvent
35
- /^emit[A-Z]/, // emitEvent
36
- ],
37
40
  // JavaScript/TypeScript
38
41
  [SupportedLanguages.JavaScript]: [
39
42
  /^use[A-Z]/, // React hooks (useEffect, etc.)
@@ -55,6 +58,16 @@ const ENTRY_POINT_PATTERNS = {
55
58
  /^build[A-Z]/, // Builder patterns
56
59
  /Service$/, // UserService
57
60
  ],
61
+ // Kotlin
62
+ [SupportedLanguages.Kotlin]: [
63
+ /^on(Create|Start|Resume|Pause|Stop|Destroy)$/, // Android lifecycle
64
+ /^do[A-Z]/, // doGet, doPost (shared JVM Servlet pattern)
65
+ /^create[A-Z]/, // Factory patterns
66
+ /^build[A-Z]/, // Builder patterns
67
+ /ViewModel$/, // MVVM pattern (Android)
68
+ /^module$/, // Ktor module entry point
69
+ /Service$/, // Service classes
70
+ ],
58
71
  // C#
59
72
  [SupportedLanguages.CSharp]: [
60
73
  /^(Get|Post|Put|Delete|Patch)/, // ASP.NET action methods
@@ -186,13 +199,10 @@ const ENTRY_POINT_PATTERNS = {
186
199
  ],
187
200
  };
188
201
  /** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
189
- const MERGED_ENTRY_POINT_PATTERNS = {};
190
- const UNIVERSAL_PATTERNS = ENTRY_POINT_PATTERNS['*'] || [];
191
- for (const [lang, patterns] of Object.entries(ENTRY_POINT_PATTERNS)) {
192
- if (lang === '*')
193
- continue;
194
- MERGED_ENTRY_POINT_PATTERNS[lang] = [...UNIVERSAL_PATTERNS, ...patterns];
195
- }
202
+ const MERGED_ENTRY_POINT_PATTERNS = Object.fromEntries(Object.keys(ENTRY_POINT_PATTERNS).map(lang => [
203
+ lang,
204
+ [...UNIVERSAL_ENTRY_POINT_PATTERNS, ...ENTRY_POINT_PATTERNS[lang]],
205
+ ]));
196
206
  // ============================================================================
197
207
  // UTILITY PATTERNS - Functions that should be penalized
198
208
  // ============================================================================
@@ -258,7 +268,7 @@ export function calculateEntryPointScore(name, language, isExported, callerCount
258
268
  }
259
269
  else {
260
270
  // Check positive patterns
261
- const allPatterns = MERGED_ENTRY_POINT_PATTERNS[language] || UNIVERSAL_PATTERNS;
271
+ const allPatterns = MERGED_ENTRY_POINT_PATTERNS[language];
262
272
  if (allPatterns.some(p => p.test(name))) {
263
273
  nameMultiplier = 1.5; // Bonus for matching entry point pattern
264
274
  reasons.push('entry-pattern');
@@ -9,6 +9,7 @@
9
9
  * DESIGN: Returns null for unknown frameworks, which causes a 1.0 multiplier
10
10
  * (no bonus, no penalty) - same behavior as before this feature.
11
11
  */
12
+ import { SupportedLanguages } from '../../config/supported-languages.js';
12
13
  export interface FrameworkHint {
13
14
  framework: string;
14
15
  entryPointMultiplier: number;
@@ -37,15 +38,22 @@ export declare const FRAMEWORK_AST_PATTERNS: {
37
38
  blazor: string[];
38
39
  efcore: string[];
39
40
  'go-http': string[];
41
+ gin: string[];
42
+ echo: string[];
43
+ fiber: string[];
44
+ 'go-grpc': string[];
40
45
  laravel: string[];
41
46
  actix: string[];
42
47
  axum: string[];
43
48
  rocket: string[];
49
+ tokio: string[];
50
+ qt: string[];
44
51
  uikit: string[];
45
52
  swiftui: string[];
46
- combine: string[];
53
+ vapor: string[];
54
+ rails: string[];
55
+ sinatra: string[];
47
56
  };
48
- import { SupportedLanguages } from '../../config/supported-languages.js';
49
57
  /**
50
58
  * Detect framework entry points from AST definition text (decorators/annotations/attributes).
51
59
  * Returns null if no known pattern is found.