ucn 4.1.3 → 4.2.1

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.
package/core/callers.js CHANGED
@@ -122,7 +122,14 @@ function getCachedCalls(index, filePath, options = {}) {
122
122
  }
123
123
  return calls;
124
124
  } catch (e) {
125
- return null;
125
+ // A missing/unreadable file, corrupt lazy cache, or parser exception
126
+ // is not equivalent to "this file has no calls". Returning null here
127
+ // let every consumer quietly continue with a plausible partial graph.
128
+ // Unsupported languages still return null through the explicit paths
129
+ // above; operational/engine failures abort with file context.
130
+ const rel = path.relative(index.root, filePath);
131
+ e.message = `getCachedCalls failed for ${rel}: ${e.message}`;
132
+ throw e;
126
133
  }
127
134
  }
128
135
 
@@ -172,15 +179,13 @@ function findCallers(index, name, options = {}) {
172
179
  definitionLines.add(`${def.file}:${def.startLine}`);
173
180
  }
174
181
 
175
- // Overload-signature identity (fix #265, zustand-measured): a pin on any
176
- // member of an overload group targets the WHOLE group the signatures
177
- // and the implementation declare one function, so a call binding the
178
- // first signature's bindingId IS a call to the pinned implementation
179
- // (useStore's only caller was excluded other-definition under the
180
- // implementation pin, a false zero-caller answer).
182
+ // Callable identity closure: overload signatures + implementation are one
183
+ // function; a TS class declaration paired with an ES5 constructor body
184
+ // (`function X(this: X, ...)`) is likewise one constructable runtime slot.
185
+ // Pinning any member must accept bindings to the compiler-equivalent one.
181
186
  if (options.targetDefinitions && options.targetDefinitions.length > 0 &&
182
187
  definitions.length > options.targetDefinitions.length) {
183
- const closed = _closeOverloadGroup(options.targetDefinitions, definitions);
188
+ const closed = _closeCallableIdentityGroup(options.targetDefinitions, definitions);
184
189
  if (closed !== options.targetDefinitions) {
185
190
  options = { ...options, targetDefinitions: closed };
186
191
  }
@@ -426,6 +431,7 @@ function findCallers(index, name, options = {}) {
426
431
  // persisted with this file's calls.
427
432
  if (call.isMethod && call.receiver &&
428
433
  (!call.receiverType || call.receiverTypeGuessed) &&
434
+ !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
429
435
  !call.receiverIsChainRoot &&
430
436
  (langTraits(fileEntry.language)?.typeSystem === 'structural' ||
431
437
  (collectAccount && !call.isPotentialCallback && !call.isPathCall &&
@@ -459,6 +465,27 @@ function findCallers(index, name, options = {}) {
459
465
  }
460
466
  }
461
467
 
468
+ // Python module-qualified constructor provenance (fix #274):
469
+ // `thread = threading.Thread(); thread.join()` is not evidence
470
+ // for a project `URL.join` merely because the parser retained
471
+ // only the terminal type name. Resolve the qualifier into the
472
+ // project when possible; external or unresolved owners remain
473
+ // visible as possible dispatch and can never enter tier 1.
474
+ if (call.isMethod && call.receiverType && call.receiverTypeQualifier &&
475
+ langTraits(fileEntry.language)?.typeSystem === 'structural') {
476
+ const origin = _structuralQualifiedReceiverOrigin(
477
+ index, fileEntry, call.receiverTypeQualifier, call.receiverType);
478
+ if (origin?.kind === 'project') {
479
+ call = { ...call, receiverTypeFlowFile: origin.fromFile };
480
+ } else if (origin?.kind === 'external') {
481
+ call = { ...call, receiverType: undefined,
482
+ receiverExternalFlow: origin.via };
483
+ } else if (origin) {
484
+ call = { ...call, receiverType: undefined,
485
+ receiverQualifiedFlow: origin.via };
486
+ }
487
+ }
488
+
462
489
  // Chained-receiver typing (fix #219): the receiver IS a call —
463
490
  // `me._def.args.parseAsync(args, params).catch(...)` — so the
464
491
  // producer's DECLARED return annotation types it (Promise →
@@ -490,12 +517,14 @@ function findCallers(index, name, options = {}) {
490
517
  }
491
518
  const folded = _foldChainedReceiverType(index, fileEntry, filePath, call, foldCtx);
492
519
  if (folded && folded.type) {
493
- call = { ...call, receiverType: folded.type };
520
+ call = { ...call, receiverType: folded.type,
521
+ ...(folded.fromFile && { receiverTypeFlowFile: folded.fromFile }) };
494
522
  } else if (folded && folded.externalVia) {
495
523
  call = { ...call, receiverExternalFlow: folded.externalVia };
496
- } else {
524
+ } else if (!folded?.suppressFallback) {
497
525
  const chainedType = _chainedReceiverType(index, call, fileEntry.language);
498
- if (chainedType) call = { ...call, receiverType: chainedType };
526
+ if (chainedType) call = { ...call, receiverType: chainedType.type,
527
+ ...(chainedType.fromFile && { receiverTypeFlowFile: chainedType.fromFile }) };
499
528
  }
500
529
  } else if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
501
530
  !call.receiverType && call.receiverCall &&
@@ -808,7 +837,10 @@ function findCallers(index, name, options = {}) {
808
837
  // (super(config) bound to the subclass's OWN constructor).
809
838
  // Super records resolve only through the parent-chain walk.
810
839
  const selfReceivers = new Set(['self', 'cls', 'this']);
811
- const skipLocalBinding = call.receiver && !selfReceivers.has(call.receiver);
840
+ const indirectStructuralReceiver = call.isMethod && !call.receiver &&
841
+ !!(call.receiverRoot || call.receiverField || call.receiverCall);
842
+ const skipLocalBinding = (call.receiver && !selfReceivers.has(call.receiver)) ||
843
+ indirectStructuralReceiver;
812
844
  if (!bindingId && !skipLocalBinding) {
813
845
  // A bare call cannot bind to a METHOD def where bare names
814
846
  // never reach methods (fix #220, cobra-measured): Go's
@@ -867,10 +899,23 @@ function findCallers(index, name, options = {}) {
867
899
  // (JS/TS `new`, Java `new`, Go/Rust composite/struct literals).
868
900
  bindingId = bindings.find(b => b.type === 'class' || b.type === 'function').id;
869
901
  } else if (bindings.length > 1 && !call.isMethod) {
870
- // For implicit same-class calls (Java: execute() means this.execute()),
871
- // try to resolve via caller's className before marking uncertain
902
+ // Function-local classes/functions with the same name
903
+ // are distinct lexical bindings. Prefer the one whose
904
+ // enclosing function is the caller's enclosing
905
+ // function before module/same-class heuristics (Click
906
+ // defines TestContext/CustomContext independently in
907
+ // several tests in one file).
872
908
  const callerSym = index.findEnclosingFunction(filePath, call.line, true);
873
- if (callerSym?.className) {
909
+ const sameLexicalOwner = callerSym ? bindings.filter(b => {
910
+ const owner = index.findEnclosingFunction(filePath, b.startLine, true);
911
+ return owner && owner.file === callerSym.file &&
912
+ owner.startLine === callerSym.startLine;
913
+ }) : [];
914
+ if (sameLexicalOwner.length === 1) {
915
+ bindingId = sameLexicalOwner[0].id;
916
+ // For implicit same-class calls (Java: execute() means
917
+ // this.execute()), try the caller class next.
918
+ } else if (callerSym?.className) {
874
919
  const callSymbols = index.symbols.get(call.name);
875
920
  const sameClassSym = callSymbols?.find(s => s.className === callerSym.className);
876
921
  if (sameClassSym) {
@@ -931,10 +976,39 @@ function findCallers(index, name, options = {}) {
931
976
  }
932
977
  }
933
978
 
979
+ // Java's bare `method()` is an implicit `this.method()`.
980
+ // If lexical binding lands on a base/interface declaration
981
+ // while the pinned target is a descendant override, runtime
982
+ // dispatch can reach that override. Keep the site visible as
983
+ // possible-dispatch instead of excluding it as another def.
984
+ if (collectAccount && bindingId && !call.isMethod && !call.receiver &&
985
+ langTraits(fileEntry.language)?.bareCallReachesMethods) {
986
+ const bound = (index.symbols.get(call.name) || [])
987
+ .find(s => s.bindingId === bindingId);
988
+ const tDefs = options.targetDefinitions || definitions;
989
+ const boundOwner = bound && (bound.className ||
990
+ (bound.receiver && bound.receiver.replace(/^\*/, '')));
991
+ const targetOwners = new Set(tDefs.map(d => d.className ||
992
+ (d.receiver && d.receiver.replace(/^\*/, ''))).filter(Boolean));
993
+ if (boundOwner && !targetOwners.has(boundOwner) &&
994
+ (_isAncestorOfTargetClass(index, boundOwner, tDefs) ||
995
+ _isDispatchAncestor(index, boundOwner, tDefs))) {
996
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
997
+ dispatchVia: boundOwner,
998
+ });
999
+ continue;
1000
+ }
1001
+ }
1002
+
934
1003
  // Smart method call handling — do this BEFORE uncertain check so
935
1004
  // self/this.method() calls can be resolved by same-class matching
936
1005
  // even when binding is ambiguous (e.g. method exists in multiple classes)
937
1006
  let resolvedBySameClass = false;
1007
+ // Python self.attr.method() resolved from __init__/field type
1008
+ // inference. It shares the routing precedence of same-class
1009
+ // resolution, but its evidence grade is receiver-hint, not
1010
+ // same-class (the receiver is the field's class).
1011
+ let resolvedByTypedAttribute = false;
938
1012
  // Receiver/path type known to mismatch the target: such an edge can
939
1013
  // never tier as confirmed even when legacy includeUncertain keeps it
940
1014
  // visible (scoreEdge checks hasReceiverType before isUncertain, so
@@ -955,6 +1029,37 @@ function findCallers(index, name, options = {}) {
955
1029
  // not confirmation evidence, not exclusion evidence. Routed
956
1030
  // method-ambiguous under the account contract.
957
1031
  let receiverTypeUnresolved = false;
1032
+ if (collectAccount && call.isMethod && !call.receiverType &&
1033
+ fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver || '')) {
1034
+ // Java permits inherited static methods to be invoked
1035
+ // through a subclass qualifier. Resolve the complete
1036
+ // receiver-owned overload family (subclass then parents)
1037
+ // before receiver-name mismatch logic. This distinguishes
1038
+ // TypeVariableName.get(Object.class) → inherited
1039
+ // TypeName.get(Type) from the subclass's inapplicable get
1040
+ // overloads.
1041
+ const callerSymbol = index.findEnclosingFunction(filePath, call.line, true) ||
1042
+ { file: filePath };
1043
+ const qualified = _calleeTypeQualifiedReceiver(
1044
+ index, callerSymbol, fileEntry, call, fileEntry.language);
1045
+ const qualifiedTargetDefs = options.targetDefinitions || definitions;
1046
+ if (qualified?.match) {
1047
+ const targetKeys = new Set(qualifiedTargetDefs.map(d => d.bindingId ||
1048
+ `${d.file}:${d.startLine}`));
1049
+ const matchKey = qualified.match.bindingId ||
1050
+ `${qualified.match.file}:${qualified.match.startLine}`;
1051
+ if (targetKeys.has(matchKey)) {
1052
+ bindingId = qualified.match.bindingId;
1053
+ resolvedBySameClass = true;
1054
+ } else {
1055
+ recordExcluded(filePath, call.line, 'other-definition');
1056
+ continue;
1057
+ }
1058
+ } else if (qualified?.unverified && !qualified.wrongArityOwner) {
1059
+ routeUnverified(filePath, fileEntry, call, qualified.unverified, calledAs);
1060
+ continue;
1061
+ }
1062
+ }
958
1063
  if (call.isMethod) {
959
1064
  if (call.selfAttribute && fileEntry.language === 'python') {
960
1065
  // self.attr.method() — resolve via attribute type inference
@@ -991,6 +1096,7 @@ function findCallers(index, name, options = {}) {
991
1096
  continue;
992
1097
  }
993
1098
  resolvedBySameClass = true;
1099
+ resolvedByTypedAttribute = true;
994
1100
  } else if (options.collectAccount || !options.includeMethods) {
995
1101
  routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
996
1102
  continue;
@@ -1179,6 +1285,21 @@ function findCallers(index, name, options = {}) {
1179
1285
  fieldDispatchType = _declaredFieldInterfaceType(index, fieldHopRootType, call.receiverField, fileEntry.language);
1180
1286
  }
1181
1287
 
1288
+ // Go package-owned value receiver (`io.Discard.Write`). The
1289
+ // imported package owns `Discard`; file/package method-name
1290
+ // bindings cannot identify its concrete type. Keep the edge
1291
+ // visible and defeat every global same-name confirmation.
1292
+ if (collectAccount && fileEntry.language === 'go' &&
1293
+ call.receiverRootIsModule) {
1294
+ const qualified = _goQualifiedReceiverType(
1295
+ index, fileEntry, call.receiverRoot, call.receiverField);
1296
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
1297
+ dispatchVia: qualified?.via || `${call.receiverRoot}.${call.receiverField}`,
1298
+ ...(qualified?.kind !== 'project' && { externalContract: true }),
1299
+ });
1300
+ continue;
1301
+ }
1302
+
1182
1303
  // Skip uncertain calls unless resolved by same-class matching or
1183
1304
  // explicitly requested. A declared-field hop type (fix #219) or
1184
1305
  // interface-field dispatch type IS receiver evidence — those
@@ -1237,10 +1358,22 @@ function findCallers(index, name, options = {}) {
1237
1358
  targetDefs.some(d => d.className === boundDef.className &&
1238
1359
  d.file === boundDef.file));
1239
1360
  }
1240
- if (!fieldHopMatchesTarget && !overloadGroupBinding) {
1361
+ // A validated self.attr receiver is the Python analogue
1362
+ // of a declared-field hop: the binding table only knows
1363
+ // the terminal name and may bind `self._live.update()` to
1364
+ // the enclosing class's `update`. Receiver type evidence
1365
+ // is stronger and must outrank that receiver-blind name
1366
+ // binding (rich Status._live -> Live, measured).
1367
+ if (!fieldHopMatchesTarget && !resolvedBySameClass && !overloadGroupBinding) {
1241
1368
  recordExcluded(filePath, call.line, 'other-definition');
1242
1369
  continue;
1243
1370
  }
1371
+ // The mismatched binding was explicitly overruled by
1372
+ // stronger receiver/overload evidence. Do not carry it
1373
+ // forward as an "exact-binding" score: it identifies a
1374
+ // different definition. The surviving edge must be scored
1375
+ // from the evidence that actually justified it.
1376
+ bindingId = null;
1244
1377
  }
1245
1378
 
1246
1379
  // Name-level import shadowing (fix #209, httpx-measured): an
@@ -1370,7 +1503,7 @@ function findCallers(index, name, options = {}) {
1370
1503
  // file imports from the target definition's file. Skips false positives like
1371
1504
  // user_b importing from b.js being reported as a caller of a.js:process.
1372
1505
  // Go/Java/Rust are excluded — they use package/module scoping, not file imports.
1373
- if (!bindingId && options.targetDefinitions && definitions.length > 1 &&
1506
+ if (!bindingId && !call.isMethod && options.targetDefinitions && definitions.length > 1 &&
1374
1507
  langTraits(fileEntry.language)?.typeSystem === 'structural') {
1375
1508
  const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
1376
1509
  if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
@@ -1687,6 +1820,28 @@ function findCallers(index, name, options = {}) {
1687
1820
  let knownType = call.receiverType || fieldHopType;
1688
1821
  if (knownType && _isGenericParamReceiverType(index, filePath, call.line, knownType)) knownType = null;
1689
1822
  if (knownType) {
1823
+ // Go package-qualified annotation identity (fix
1824
+ // #273): `next http.Handler` is not a project-local
1825
+ // `Handler`. External/opaque packages may expose an
1826
+ // interface (implicit satisfaction), and a project
1827
+ // interface is explicitly a dispatch contract.
1828
+ // Both stay visible; neither can confirm a pinned
1829
+ // concrete implementation.
1830
+ if (collectAccount && fileEntry.language === 'go' &&
1831
+ call.receiverTypeQualifier) {
1832
+ const qualified = _goQualifiedReceiverType(
1833
+ index, fileEntry, call.receiverTypeQualifier, knownType);
1834
+ const isContract = qualified?.kind === 'project' &&
1835
+ qualified.defs.some(d => d.type === 'interface' || d.type === 'trait');
1836
+ if (qualified && (qualified.kind !== 'project' || isContract)) {
1837
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
1838
+ dispatchVia: qualified.via,
1839
+ dispatchCandidates: countDispatchCandidates(knownType),
1840
+ ...(qualified.kind !== 'project' && { externalContract: true }),
1841
+ });
1842
+ continue;
1843
+ }
1844
+ }
1690
1845
  const viaFieldHop = !call.receiverType; // declared-field hop (fix #202)
1691
1846
  // Exclusion requires an UNRELATED type. A receiver typed
1692
1847
  // as an ANCESTOR of the target's class may dynamically
@@ -1697,7 +1852,8 @@ function findCallers(index, name, options = {}) {
1697
1852
  const structural = langTraits(fileEntry.language)?.typeSystem === 'structural';
1698
1853
  if (targetTypes.has(knownType)) {
1699
1854
  receiverTypeValidated = true;
1700
- // Identity discipline (nominal, fix #206): a
1855
+ // Identity discipline (fix #206, extended to
1856
+ // structural flow in #272): a
1701
1857
  // NAME match is only identity when the
1702
1858
  // unqualified type name resolves (same file →
1703
1859
  // same package directory → import edge) to the
@@ -1722,6 +1878,26 @@ function findCallers(index, name, options = {}) {
1722
1878
  receiverTypeValidated = false;
1723
1879
  receiverTypeUnresolved = true;
1724
1880
  }
1881
+ } else if (structural) {
1882
+ // Multiple project types may share the
1883
+ // same class name across packages/versions
1884
+ // (zod v3 + v4 both define ZodArray). A
1885
+ // folded/module return type carries the
1886
+ // annotation's origin file; use it before
1887
+ // accepting the name as target identity.
1888
+ const sameNameTypeDefs = (index.symbols.get(knownType) || [])
1889
+ .filter(d => IDENTITY_TYPE_KINDS.has(d.type));
1890
+ if (sameNameTypeDefs.length > 1 ||
1891
+ (call.receiverTypeQualifier && call.receiverTypeFlowFile)) {
1892
+ const identity = _resolveStructuralFlowTypeIdentity(
1893
+ index, call.receiverTypeFlowFile || filePath, knownType, targetDefs);
1894
+ if (identity === 'other') {
1895
+ receiverTypeValidated = false;
1896
+ } else if (identity === 'unknown') {
1897
+ receiverTypeValidated = false;
1898
+ receiverTypeUnresolved = true;
1899
+ }
1900
+ }
1725
1901
  }
1726
1902
  }
1727
1903
  const matchesTarget = receiverTypeValidated ||
@@ -1916,7 +2092,9 @@ function findCallers(index, name, options = {}) {
1916
2092
  // call.receiver guard: a generic-param knownType
1917
2093
  // (fix #220) reaches here receiver-less — there is
1918
2094
  // no receiver NAME to match against.
1919
- if (call.receiver && !inferredMatch && !inferredMismatch && definitions.length > 1 && !fieldDispatchType) {
2095
+ if (call.receiver && !inferredMatch && !inferredMismatch &&
2096
+ definitions.length > 1 && !fieldDispatchType &&
2097
+ !call.receiverExternalFlow) {
1920
2098
  const receiverLower = call.receiver.toLowerCase();
1921
2099
  const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
1922
2100
  // Type-qualified identity discipline (fix #220,
@@ -2559,6 +2737,12 @@ function findCallers(index, name, options = {}) {
2559
2737
  });
2560
2738
  continue;
2561
2739
  }
2740
+ if (!typeQualifiedReceiver && call.receiverQualifiedFlow) {
2741
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
2742
+ dispatchVia: call.receiverQualifiedFlow,
2743
+ });
2744
+ continue;
2745
+ }
2562
2746
  // Builtin-global receiver (fix #232, campaign-measured:
2563
2747
  // console.log() confirmed scope-match against a private
2564
2748
  // Logger.log — its single project-wide owner). console/
@@ -2709,13 +2893,13 @@ function findCallers(index, name, options = {}) {
2709
2893
  calledAs,
2710
2894
  _evidence: {
2711
2895
  hasBindingId: !!bindingId,
2712
- resolvedBySameClass: !!resolvedBySameClass,
2896
+ resolvedBySameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
2713
2897
  hasSamePackageEvidence,
2714
2898
  // Method calls where binding resolution was skipped (non-self receiver)
2715
2899
  // and the receiver has no binding evidence → uncertain (JS/TS/Python only)
2716
2900
  isUncertain: !!isUncertain || uncertainMethodReceiver,
2717
2901
  hasReceiverType: langTraits(fileEntry.language)?.typeSystem === 'structural'
2718
- ? receiverTypeValidated
2902
+ ? (receiverTypeValidated || resolvedByTypedAttribute)
2719
2903
  : !!call.receiverType,
2720
2904
  hasReceiverEvidence: !!(call.receiver &&
2721
2905
  (fileEntry.bindings || []).some(b => b.name === call.receiver)),
@@ -2726,8 +2910,12 @@ function findCallers(index, name, options = {}) {
2726
2910
  pendingCount++;
2727
2911
  }
2728
2912
  } catch (e) {
2729
- // Expected: minified files exceed tree-sitter buffer, binary files fail to parse.
2730
- // These are not actionable errors silently skip.
2913
+ // Parsing/read failures are contained by getCachedCalls(). Any
2914
+ // exception that reaches this semantic loop is an engine defect;
2915
+ // returning a partial caller set would be a high-trust lie.
2916
+ const rel = path.relative(index.root, filePath);
2917
+ e.message = `findCallers failed while analyzing ${rel}: ${e.message}`;
2918
+ throw e;
2731
2919
  }
2732
2920
  }
2733
2921
 
@@ -2751,7 +2939,7 @@ function findCallers(index, name, options = {}) {
2751
2939
  let unverifiedEnriched = 0;
2752
2940
 
2753
2941
  // Phase 2: Read content only for files with matching calls (eliminates ~98% of file reads)
2754
- outer: for (const [filePath, pending] of pendingByFile) {
2942
+ for (const [filePath, pending] of pendingByFile) {
2755
2943
  let content = null;
2756
2944
  for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType, calledAs, _evidence, _tier, _reason, _meta } of pending) {
2757
2945
  const scored = scoreEdge(_evidence || {});
@@ -2769,6 +2957,8 @@ function findCallers(index, name, options = {}) {
2769
2957
  relativePath: fileEntry.relativePath,
2770
2958
  line: call.line,
2771
2959
  confidence: scored.confidence,
2960
+ evidenceScore: scored.evidenceScore,
2961
+ scoreKind: scored.scoreKind,
2772
2962
  resolution: scored.resolution,
2773
2963
  tier: _tier,
2774
2964
  reason: _reason,
@@ -2782,7 +2972,10 @@ function findCallers(index, name, options = {}) {
2782
2972
  if (unverifiedEnriched < unverifiedEnrichLimit) {
2783
2973
  if (content === null) {
2784
2974
  try { content = fs.readFileSync(filePath, 'utf-8'); }
2785
- catch (e) { content = ''; }
2975
+ catch (e) {
2976
+ e.message = `caller enrichment read failed: ${e.message}`;
2977
+ throw e;
2978
+ }
2786
2979
  }
2787
2980
  const enclosing = index.findEnclosingFunction(filePath, call.line, true);
2788
2981
  unverifiedEntries.push({
@@ -2812,6 +3005,8 @@ function findCallers(index, name, options = {}) {
2812
3005
  relativePath: fileEntry.relativePath,
2813
3006
  line: call.line,
2814
3007
  confidence: scored.confidence,
3008
+ evidenceScore: scored.evidenceScore,
3009
+ scoreKind: scored.scoreKind,
2815
3010
  resolution: scored.resolution,
2816
3011
  ...(tier && { tier }),
2817
3012
  isMethod: call.isMethod || false,
@@ -2825,7 +3020,10 @@ function findCallers(index, name, options = {}) {
2825
3020
  // First time we hit this file's enrichment loop — read the file once.
2826
3021
  if (content === null) {
2827
3022
  try { content = fs.readFileSync(filePath, 'utf-8'); }
2828
- catch (e) { content = ''; /* deleted/unreadable; skip enrichment for rest */ break; }
3023
+ catch (e) {
3024
+ e.message = `caller enrichment read failed: ${e.message}`;
3025
+ throw e;
3026
+ }
2829
3027
  }
2830
3028
  callers.push({
2831
3029
  file: filePath,
@@ -2842,6 +3040,8 @@ function findCallers(index, name, options = {}) {
2842
3040
  ...(receiverType && { receiverType }),
2843
3041
  ...(edgeCalledAs && { calledAs: edgeCalledAs }),
2844
3042
  confidence: scored.confidence,
3043
+ evidenceScore: scored.evidenceScore,
3044
+ scoreKind: scored.scoreKind,
2845
3045
  resolution: scored.resolution,
2846
3046
  ...(tier && { tier }),
2847
3047
  });
@@ -2905,20 +3105,40 @@ function findCallers(index, name, options = {}) {
2905
3105
  * - Python: receiverType from __init__ attribute type inference (getInstanceAttributeTypes)
2906
3106
  *
2907
3107
  * @param {object} index - ProjectIndex instance
2908
- * @param {object} def - Symbol definition with file, name, startLine, endLine
3108
+ * @param {object|string} definition - Symbol definition or resolvable symbol name
2909
3109
  * @param {object} [options] - Options
2910
3110
  * @param {boolean} [options.includeMethods] - Include method calls (default: false)
2911
3111
  */
2912
- function findCallees(index, def, options = {}) {
3112
+ function findCallees(index, definition, options = {}) {
2913
3113
  index._beginOp();
2914
3114
  try {
3115
+ const def = typeof definition === 'string'
3116
+ ? index.resolveSymbol(definition, options).def
3117
+ : definition;
3118
+ if (!def) return [];
3119
+ if (!def.file) throw new TypeError('findCallees requires a definition with a file');
2915
3120
  // Lazy-load callsCache from disk if not already populated
2916
3121
  if (index.loadCallsCache) index.loadCallsCache();
2917
3122
 
2918
3123
  try {
2919
3124
  // Get all calls from the file's cache (now includes enclosingFunction)
2920
- const calls = getCachedCalls(index, def.file);
2921
- if (!calls) return [];
3125
+ const allCalls = getCachedCalls(index, def.file);
3126
+ if (!allCalls) return [];
3127
+
3128
+ // A file can contain hundreds of symbols and thousands of calls.
3129
+ // Reachability invokes findCallees once per reachable symbol; scanning
3130
+ // the full file for every symbol made that walk O(symbols*calls)
3131
+ // (14-16s on clap). Restrict the resolution loop and local-type scan
3132
+ // to this definition's source range. Nested closures deliberately
3133
+ // remain in the slice and retain the existing inner-symbol rules.
3134
+ const calls = _callsInDefinitionRange(index, def.file, allCalls,
3135
+ def.startLine, def.endLine);
3136
+ // The reachability walk uses the legacy (non-accounting) path and most
3137
+ // entry/test symbols contain no calls. Avoid constructing receiver,
3138
+ // overload, and flow machinery for an empty source range. Contract
3139
+ // callers still run below so they receive the explicit zero-site
3140
+ // callee account object.
3141
+ if (calls.length === 0 && !options.collectAccount) return [];
2922
3142
 
2923
3143
  // Get file language for smart method call handling
2924
3144
  const fileEntry = index.files.get(def.file);
@@ -2928,12 +3148,9 @@ function findCallees(index, def, options = {}) {
2928
3148
  // Only class methods are excluded — they are independently addressable symbols.
2929
3149
  // Calls within closures (named functions without className) ARE included as
2930
3150
  // callees of the parent function, since closures are part of the parent's behavior.
2931
- const innerSymbolRanges = fileEntry ? fileEntry.symbols
2932
- .filter(s => !NON_CALLABLE_TYPES.has(s.type) &&
2933
- s.className && // Only exclude class methods, not closures
2934
- s.startLine > def.startLine && s.endLine <= def.endLine &&
2935
- s.startLine !== def.startLine)
2936
- .map(s => [s.startLine, s.endLine]) : [];
3151
+ const innerSymbolRanges = fileEntry
3152
+ ? _innerClassMethodRanges(index, def, fileEntry)
3153
+ : [];
2937
3154
 
2938
3155
  const callees = new Map(); // key -> { name, bindingId, count }
2939
3156
  let selfAttrCalls = null; // collected for Python self.attr.method() resolution
@@ -2984,20 +3201,71 @@ function findCallees(index, def, options = {}) {
2984
3201
  };
2985
3202
  // Retain an uncertain/unresolved call as a visible unverified callee
2986
3203
  // entry (aggregated by name+reason) and claim its site.
2987
- const noteUnverified = (siteId, call, reason) => {
3204
+ const noteUnverified = (siteId, call, reason, meta = {}) => {
2988
3205
  if (!collectAccount || claimedSiteIds.has(siteId)) return;
2989
3206
  noteSite(siteId, 'unverified', reason, call);
2990
- const key = `${call.name}|${reason}`;
3207
+ const key = `${call.name}|${reason}|${meta.dispatchVia || ''}`;
2991
3208
  let entry = unverifiedCallees.get(key);
2992
3209
  if (!entry) {
2993
3210
  const defs = index.symbols.get(call.name) || [];
2994
3211
  const owners = defs.filter(s => !NON_CALLABLE_TYPES.has(s.type)).length;
2995
- entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners };
3212
+ entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners, ...meta };
2996
3213
  unverifiedCallees.set(key, entry);
2997
3214
  }
2998
3215
  entry.callCount++;
2999
3216
  entry.sites.push(call.line);
3000
3217
  };
3218
+ // A statically selected base implementation is not the only runtime
3219
+ // target in languages with virtual/structural dispatch. If a project
3220
+ // descendant overrides the method, the call site must stay visible as
3221
+ // possible-dispatch instead of being presented as an exact callee.
3222
+ // This is contract-only so legacy traversal remains byte-compatible.
3223
+ const routeVirtualOverride = (siteId, call, receiverType, match) => {
3224
+ if (!collectAccount || !match) return false;
3225
+ const traits = langTraits(language);
3226
+ const matchedOwner = match.className || (match.receiver && match.receiver.replace(/^\*/, ''));
3227
+ if (!matchedOwner) return false;
3228
+
3229
+ let dispatchBase = receiverType || matchedOwner;
3230
+ const explicitContract = _isDispatchContractType(index, dispatchBase);
3231
+ // Go is nominal in UCN's receiver-resolution taxonomy, but its
3232
+ // interfaces dispatch structurally and are satisfied implicitly.
3233
+ // A project interface is therefore a virtual boundary even though
3234
+ // there is no implements edge in the graph.
3235
+ if (traits?.typeSystem !== 'structural' && !traits?.allMethodsVirtual &&
3236
+ !explicitContract) return false;
3237
+ if (explicitContract) {
3238
+ const candidates = _countDispatchCandidates(
3239
+ index, dispatchBase, index.symbols.get(call.name) || []);
3240
+ noteUnverified(siteId, call, 'possible-dispatch', {
3241
+ dispatchVia: dispatchBase,
3242
+ dispatchCandidates: Math.max(1, candidates),
3243
+ });
3244
+ return true;
3245
+ }
3246
+ let owners = _descendantOverrideOwners(index, dispatchBase, call.name);
3247
+ // Structural aliases/generic heads (for example ZodTypeAny) may
3248
+ // have no inheritance node of their own. When the statically
3249
+ // selected declaration lives on an ancestor, use that declaration
3250
+ // owner as the conservative virtual-dispatch boundary. Do not do
3251
+ // this for a concrete class: an exact Child receiver cannot become
3252
+ // an unrelated sibling of Child.
3253
+ if (owners.size === 0 && dispatchBase !== matchedOwner) {
3254
+ const typeDefs = index.symbols.get(dispatchBase) || [];
3255
+ const isConcreteClass = typeDefs.some(d =>
3256
+ d.type === 'class' || d.type === 'struct' || d.type === 'impl');
3257
+ if (!isConcreteClass) {
3258
+ dispatchBase = matchedOwner;
3259
+ owners = _descendantOverrideOwners(index, dispatchBase, call.name);
3260
+ }
3261
+ }
3262
+ if (owners.size === 0) return false;
3263
+ noteUnverified(siteId, call, 'possible-dispatch', {
3264
+ dispatchVia: dispatchBase,
3265
+ dispatchCandidates: owners.size + 1,
3266
+ });
3267
+ return true;
3268
+ };
3001
3269
 
3002
3270
  // Build local variable type map for receiver resolution
3003
3271
  // Scans for patterns like: bt = Backtester(...) → bt maps to Backtester
@@ -3012,7 +3280,7 @@ function findCallees(index, def, options = {}) {
3012
3280
  // resolution needs the external-producer/typed-receiver defeater).
3013
3281
  let _flowMap;
3014
3282
  const flowMap = () => {
3015
- if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file, calls);
3283
+ if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file, allCalls);
3016
3284
  return _flowMap;
3017
3285
  };
3018
3286
  // Chained-receiver fold context (fix #268 — the #258 rails, callee
@@ -3044,6 +3312,16 @@ function findCallees(index, def, options = {}) {
3044
3312
  if (!isDirectMatch && !isNestedCallback) continue;
3045
3313
  if (calleeAccount) calleeAccount.totalSites++;
3046
3314
 
3315
+ // Query-time return flow for an ordinary receiver assignment:
3316
+ // `v := New(); v.ReadConfig()`. The compiler-declared return type
3317
+ // is stronger than constructor-name guesses and must participate
3318
+ // in callee identity just as it already does for callers.
3319
+ const directReceiverFlow = call.isMethod && call.receiver && !call.receiverType &&
3320
+ !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
3321
+ !['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver)
3322
+ ? _lookupReturnTypeFlow(flowMap(), call)
3323
+ : undefined;
3324
+
3047
3325
  // Declared-field receiver hop (fix #231 — callee-side parity
3048
3326
  // with the caller side's #202/#219): `tm.service.Save()` /
3049
3327
  // `this._map.has()` records carry receiverRoot/receiverField —
@@ -3056,8 +3334,19 @@ function findCallees(index, def, options = {}) {
3056
3334
  // trait-typed and generic-param fields return null.
3057
3335
  let fieldHopType = null;
3058
3336
  let fieldHopInfo = null;
3337
+ let fieldDispatchType = null;
3059
3338
  if (call.isMethod && !call.receiverType && call.receiverField) {
3060
3339
  let hopRoot = call.receiverRootType;
3340
+ if (!hopRoot && call.receiverRoot && localTypes?.has(call.receiverRoot)) {
3341
+ hopRoot = localTypes.get(call.receiverRoot);
3342
+ }
3343
+ if (!hopRoot && call.receiverRoot) {
3344
+ const inferredRoot = _lookupReturnTypeFlow(flowMap(), {
3345
+ ...call,
3346
+ receiver: call.receiverRoot,
3347
+ });
3348
+ if (inferredRoot?.type) hopRoot = inferredRoot.type;
3349
+ }
3061
3350
  if (!hopRoot && call.receiverRoot === 'this' &&
3062
3351
  langTraits(language)?.typeSystem === 'structural') {
3063
3352
  hopRoot = index.findEnclosingFunction(def.file, call.line, true)?.className;
@@ -3065,9 +3354,78 @@ function findCallees(index, def, options = {}) {
3065
3354
  if (hopRoot) {
3066
3355
  fieldHopInfo = {};
3067
3356
  fieldHopType = _declaredFieldType(index, hopRoot, call.receiverField, language, fieldHopInfo);
3357
+ if (!fieldHopType) {
3358
+ fieldDispatchType = _declaredFieldInterfaceType(
3359
+ index, hopRoot, call.receiverField, language);
3360
+ }
3068
3361
  }
3069
3362
  }
3070
3363
 
3364
+ if (fieldDispatchType) {
3365
+ noteUnverified(siteId, call, 'possible-dispatch', {
3366
+ dispatchVia: fieldDispatchType,
3367
+ dispatchCandidates: _countDispatchCandidates(
3368
+ index, fieldDispatchType, index.symbols.get(call.name) || []),
3369
+ });
3370
+ continue;
3371
+ }
3372
+
3373
+ if (collectAccount && language === 'go' && call.receiverRootIsModule) {
3374
+ const qualified = _goQualifiedReceiverType(
3375
+ index, fileEntry, call.receiverRoot, call.receiverField);
3376
+ if (_calleeZeroCandidateName(index, call)) {
3377
+ noteSite(siteId, 'external', null, call);
3378
+ } else {
3379
+ noteUnverified(siteId, call, 'possible-dispatch', {
3380
+ dispatchVia: qualified?.via || `${call.receiverRoot}.${call.receiverField}`,
3381
+ ...(qualified?.kind !== 'project' && { externalContract: true }),
3382
+ });
3383
+ }
3384
+ continue;
3385
+ }
3386
+
3387
+ // Macro invocations occupy their own callable namespace/shape.
3388
+ // A same-named method or function is never the macro callee
3389
+ // (`arg!(...)` beside `.arg(...)` was resolving to ArgGroup.arg
3390
+ // in clap). Resolve a unique project macro exactly; ambiguity is
3391
+ // visible under the account contract, and absent macros are
3392
+ // external. Never fall through to generic name ranking.
3393
+ if (call.isMacro) {
3394
+ const macroDefs = (index.symbols.get(call.name) || []).filter(s =>
3395
+ s.type === 'macro' && _calleeLanguageCompatible(index, s, language));
3396
+ const uniqueMacro = macroDefs.length === 1 ? macroDefs[0] : null;
3397
+ const macroInScope = !!uniqueMacro && (
3398
+ (uniqueMacro.file === def.file && uniqueMacro.startLine <= call.line) ||
3399
+ !!call.receiver ||
3400
+ (fileEntry?.importBindings || []).some(b => b.name === call.name));
3401
+ if (uniqueMacro && macroInScope) {
3402
+ const match = uniqueMacro;
3403
+ const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
3404
+ const existing = callees.get(key);
3405
+ if (existing) {
3406
+ existing.count += 1;
3407
+ if (collectAccount) {
3408
+ existing.sites.push(call.line);
3409
+ existing.siteIds.push(siteId);
3410
+ }
3411
+ } else {
3412
+ callees.set(key, {
3413
+ name: call.name,
3414
+ bindingId: match.bindingId,
3415
+ count: 1,
3416
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }),
3417
+ });
3418
+ }
3419
+ } else if (macroDefs.length > 1) {
3420
+ noteUnverified(siteId, call, 'macro-ambiguous');
3421
+ } else if (uniqueMacro) {
3422
+ noteUnverified(siteId, call, 'macro-scope-unknown');
3423
+ } else {
3424
+ noteSite(siteId, 'external', null, call);
3425
+ }
3426
+ continue;
3427
+ }
3428
+
3071
3429
  // Go package-qualified receiver: resolve the import module up
3072
3430
  // front so the dispatch chain can tell package calls apart from
3073
3431
  // type-qualified method expressions (fix #236 — a receiver that
@@ -3075,18 +3433,25 @@ function findCallees(index, def, options = {}) {
3075
3433
  let goImportModule = null;
3076
3434
  if (call.receiver && langTraits(language)?.hasReceiverPackageCalls) {
3077
3435
  const goImports = fileEntry?.imports || [];
3436
+ const goImportNames = fileEntry?.importNames || [];
3078
3437
  // Handle Go version suffixes: k8s.io/klog/v2 → klog, not v2
3079
3438
  // fix #268 (chi-measured): computed for NON-method records
3080
3439
  // too — the parser marks `context.WithValue(...)` isMethod:
3081
3440
  // false, which used to skip the whole package-ownership
3082
3441
  // dispatch and confirm the bare name against the project's
3083
3442
  // only WithValue def (middleware/value.go).
3084
- goImportModule = goImports.find(mod => {
3085
- const parts = mod.split('/');
3086
- const last = parts[parts.length - 1];
3087
- const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
3088
- return pkgName === call.receiver;
3089
- }) || null;
3443
+ if (goImportNames.length === goImports.length) {
3444
+ const importIndex = goImportNames.indexOf(call.receiver);
3445
+ if (importIndex >= 0) goImportModule = goImports[importIndex];
3446
+ }
3447
+ if (!goImportModule) {
3448
+ goImportModule = goImports.find(mod => {
3449
+ const parts = mod.split('/');
3450
+ const last = parts[parts.length - 1];
3451
+ const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
3452
+ return pkgName === call.receiver;
3453
+ }) || null;
3454
+ }
3090
3455
  }
3091
3456
 
3092
3457
  // Type-qualified receiver resolution (fix #236): the receiver
@@ -3140,16 +3505,44 @@ function findCallees(index, def, options = {}) {
3140
3505
  // Rust Self::method() resolves same-impl the same way (fix #236, the #232 callee analog)
3141
3506
  } else if (call.receiver === 'super') {
3142
3507
  // super().method() — resolve to parent class method below
3143
- } else if (localTypes && localTypes.has(call.receiver)) {
3508
+ } else if (directReceiverFlow?.externalVia) {
3509
+ noteUnverified(siteId, call, 'possible-dispatch', {
3510
+ dispatchVia: directReceiverFlow.externalVia,
3511
+ externalContract: true,
3512
+ });
3513
+ continue;
3514
+ } else if (localTypes && localTypes.has(call.receiver) && !directReceiverFlow?.type) {
3144
3515
  // Resolve method calls on locally-constructed objects:
3145
3516
  // bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
3146
3517
  // Go: f.Run() where f is *Framework → Framework.Run (receiver match)
3147
3518
  const typeName = localTypes.get(call.receiver);
3148
3519
  const symbols = index.symbols.get(call.name);
3520
+ const qualifiedType = language === 'go'
3521
+ ? _goQualifiedReceiverType(index, fileEntry,
3522
+ call.receiverTypeQualifier, typeName)
3523
+ : null;
3524
+ const qualifiedContract = qualifiedType?.kind === 'project' &&
3525
+ qualifiedType.defs.some(d => d.type === 'interface' || d.type === 'trait');
3526
+ if (qualifiedType && (qualifiedType.kind !== 'project' || qualifiedContract)) {
3527
+ noteUnverified(siteId, call, 'possible-dispatch', {
3528
+ dispatchVia: qualifiedType.via,
3529
+ dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
3530
+ ...(qualifiedType.kind !== 'project' && { externalContract: true }),
3531
+ });
3532
+ continue;
3533
+ }
3534
+ if (!qualifiedType && _isDispatchContractType(index, typeName)) {
3535
+ noteUnverified(siteId, call, 'possible-dispatch', {
3536
+ dispatchVia: typeName,
3537
+ dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
3538
+ });
3539
+ continue;
3540
+ }
3149
3541
  const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
3150
3542
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
3151
3543
  const onClass = (cls) => (symbols || []).filter(s =>
3152
- isCallable(s) && (
3544
+ isCallable(s) &&
3545
+ (!qualifiedType?.dir || (s.file && path.dirname(s.file) === qualifiedType.dir)) && (
3153
3546
  s.className === cls ||
3154
3547
  (s.receiver && s.receiver.replace(/^\*/, '') === cls)));
3155
3548
  // Same-class overloads select by static call shape —
@@ -3172,6 +3565,7 @@ function findCallees(index, def, options = {}) {
3172
3565
  }
3173
3566
  const match = sel.match;
3174
3567
  if (match) {
3568
+ if (routeVirtualOverride(siteId, call, typeName, match)) continue;
3175
3569
  const key = match.bindingId || `${typeName}.${call.name}`;
3176
3570
  const existing = callees.get(key);
3177
3571
  if (existing) {
@@ -3199,7 +3593,8 @@ function findCallees(index, def, options = {}) {
3199
3593
  noteUnverified(siteId, call, 'uncertain-receiver');
3200
3594
  }
3201
3595
  continue;
3202
- } else if (call.receiverType || fieldHopType || fieldHopInfo?.externalVia) {
3596
+ } else if (call.receiverType || directReceiverFlow?.type ||
3597
+ fieldHopType || fieldHopInfo?.externalVia) {
3203
3598
  // Use parser-inferred receiverType for method resolution
3204
3599
  // Go/Java/Rust: from param/receiver type declarations
3205
3600
  // JS/TS: from `new Foo()` assignments or TypeScript type annotations
@@ -3221,12 +3616,36 @@ function findCallees(index, def, options = {}) {
3221
3616
  }
3222
3617
  continue;
3223
3618
  }
3224
- const typeName = call.receiverType || fieldHopType;
3619
+ const typeName = call.receiverType || directReceiverFlow?.type || fieldHopType;
3225
3620
  const symbols = index.symbols.get(call.name);
3621
+ const qualifiedType = language === 'go' && call.receiverType
3622
+ ? _goQualifiedReceiverType(index, fileEntry,
3623
+ call.receiverTypeQualifier, typeName)
3624
+ : null;
3625
+ const qualifiedContract = qualifiedType?.kind === 'project' &&
3626
+ qualifiedType.defs.some(d => d.type === 'interface' || d.type === 'trait');
3627
+ if (qualifiedType && (qualifiedType.kind !== 'project' || qualifiedContract)) {
3628
+ noteUnverified(siteId, call, 'possible-dispatch', {
3629
+ dispatchVia: qualifiedType.via,
3630
+ dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
3631
+ ...(qualifiedType.kind !== 'project' && { externalContract: true }),
3632
+ });
3633
+ continue;
3634
+ }
3635
+ if (!qualifiedType && _isDispatchContractType(index, typeName)) {
3636
+ noteUnverified(siteId, call, 'possible-dispatch', {
3637
+ dispatchVia: typeName,
3638
+ dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
3639
+ });
3640
+ continue;
3641
+ }
3226
3642
  const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
3227
3643
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
3228
3644
  const onClassRT = (cls) => (symbols || []).filter(s =>
3229
- isCallableRT(s) && (
3645
+ isCallableRT(s) &&
3646
+ (!qualifiedType?.dir || (s.file && path.dirname(s.file) === qualifiedType.dir)) &&
3647
+ (!directReceiverFlow?.fromFile || !s.file ||
3648
+ path.dirname(s.file) === path.dirname(directReceiverFlow.fromFile)) && (
3230
3649
  (s.receiver && s.receiver.replace(/^\*/, '') === cls) ||
3231
3650
  s.className === cls));
3232
3651
  // Same-class overload selection by static call shape (fix #268)
@@ -3247,6 +3666,7 @@ function findCallees(index, def, options = {}) {
3247
3666
  }
3248
3667
  const match = sel.match;
3249
3668
  if (match) {
3669
+ if (routeVirtualOverride(siteId, call, typeName, match)) continue;
3250
3670
  const key = match.bindingId || `${typeName}.${call.name}`;
3251
3671
  const existing = callees.get(key);
3252
3672
  if (existing) {
@@ -3285,8 +3705,31 @@ function findCallees(index, def, options = {}) {
3285
3705
  }
3286
3706
  continue;
3287
3707
  }
3708
+ if (qualifiedType?.kind === 'project') {
3709
+ // The import pins this receiver to a concrete project
3710
+ // package, but no exact member body is indexed there.
3711
+ // Promotion/generation may explain the gap; never fall
3712
+ // through to an unrelated global same-name method.
3713
+ noteUnverified(siteId, call, 'uncertain-receiver', {
3714
+ dispatchVia: qualifiedType.via,
3715
+ });
3716
+ continue;
3717
+ }
3718
+ // An unindexed Go receiver type may be an interface from
3719
+ // another package. Go satisfaction is implicit, so a
3720
+ // same-name project method is a possible runtime target,
3721
+ // never a sound bare-name confirmation or exclusion.
3722
+ if (collectAccount && language === 'go' &&
3723
+ _externalGoDispatchType(index, typeName)) {
3724
+ noteUnverified(siteId, call, 'possible-dispatch', {
3725
+ dispatchVia: typeName,
3726
+ dispatchCandidates: _countDispatchCandidates(
3727
+ index, typeName, symbols || []),
3728
+ });
3729
+ continue;
3730
+ }
3288
3731
  // No match found with inferred type — fall through to include as unresolved
3289
- } else if (call.receiverCall && !call.receiver && !call.isConstructor &&
3732
+ } else if (call.receiverCall && (!call.receiver || call.receiverIsChainRoot) && !call.isConstructor &&
3290
3733
  langTraits(language)?.typeSystem === 'nominal') {
3291
3734
  // Chained receiver, nominal callee direction (fix #268,
3292
3735
  // chi-measured): `m.NotFound().ServeHTTP(w, r)` /
@@ -3320,6 +3763,7 @@ function findCallees(index, def, options = {}) {
3320
3763
  }
3321
3764
  if (sel.match) {
3322
3765
  const match = sel.match;
3766
+ if (routeVirtualOverride(siteId, call, chained.type, match)) continue;
3323
3767
  const key = match.bindingId || `${chained.type}.${call.name}`;
3324
3768
  const existing = callees.get(key);
3325
3769
  if (existing) {
@@ -3394,6 +3838,33 @@ function findCallees(index, def, options = {}) {
3394
3838
  noteSite(siteId, 'filtered', 'method-calls-excluded', call);
3395
3839
  continue;
3396
3840
  }
3841
+
3842
+ // Contract surface: an untyped nominal receiver with several
3843
+ // project owner types has no exact callee identity. File or
3844
+ // package bindings name method declarations, not the runtime
3845
+ // type of `h` in `h.ServeHTTP(...)`; letting the generic
3846
+ // lexical pass choose one fabricates a concrete edge.
3847
+ if (collectAccount && call.receiver && call.receiverExternalFlow) {
3848
+ noteUnverified(siteId, call, 'possible-dispatch', {
3849
+ dispatchVia: call.receiverExternalFlow,
3850
+ externalContract: true,
3851
+ });
3852
+ continue;
3853
+ }
3854
+ if (collectAccount && call.receiver && !call.receiverType &&
3855
+ !call.receiverCall && !call.isPotentialCallback &&
3856
+ !['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver)) {
3857
+ const owners = new Set((index.symbols.get(call.name) || [])
3858
+ .filter(s => !NON_CALLABLE_TYPES.has(s.type))
3859
+ .map(s => s.className || (s.receiver && s.receiver.replace(/^\*/, '')))
3860
+ .filter(Boolean));
3861
+ if (owners.size > 1) {
3862
+ noteUnverified(siteId, call, 'method-ambiguous', {
3863
+ dispatchCandidates: owners.size,
3864
+ });
3865
+ continue;
3866
+ }
3867
+ }
3397
3868
  }
3398
3869
 
3399
3870
  // Skip keywords and built-ins — EXCEPT self/super-received method
@@ -3404,7 +3875,14 @@ function findCallees(index, def, options = {}) {
3404
3875
  const selfShaped = call.isMethod &&
3405
3876
  (['self', 'cls', 'this', 'super'].includes(call.receiver) ||
3406
3877
  (call.receiver === 'Self' && language === 'rust'));
3407
- if (!selfShaped && index.isKeyword(call.name, language)) {
3878
+ // Builtin/global names are shadowable. `Request` is a web global,
3879
+ // but `const Request = require('./request')` owns `new Request()`
3880
+ // in that module. Resolve explicit lexical/import bindings before
3881
+ // classifying a same-named builtin as external.
3882
+ const shadowsBuiltin = !call.receiver && (
3883
+ fileEntry?.importBindings?.some(b => b.name === call.name) ||
3884
+ fileEntry?.bindings?.some(b => b.name === call.name));
3885
+ if (!selfShaped && !shadowsBuiltin && index.isKeyword(call.name, language)) {
3408
3886
  noteSite(siteId, 'external', null, call);
3409
3887
  continue;
3410
3888
  }
@@ -3463,20 +3941,93 @@ function findCallees(index, def, options = {}) {
3463
3941
  continue;
3464
3942
  }
3465
3943
 
3944
+ // Structural module ownership in the callee direction (the
3945
+ // caller-side #209/#217 rule). `@click.group()` resolves through
3946
+ // the imported `click` module; a same-named local decorated
3947
+ // function named `group` is receiver-blind and must never steal
3948
+ // the callee. Follow the module's name-level re-export chain and
3949
+ // add only definitions it actually exposes. Unknown CJS/dynamic
3950
+ // surfaces stay visible; external modules are external.
3951
+ if (call.isMethod && call.receiverIsModule &&
3952
+ langTraits(language)?.typeSystem === 'structural') {
3953
+ const moduleRoute = _calleeStructuralModuleRoute(index, fileEntry, call, language);
3954
+ if (moduleRoute.matches?.length) {
3955
+ for (const match of moduleRoute.matches) {
3956
+ const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
3957
+ const existing = callees.get(key);
3958
+ if (existing) {
3959
+ existing.count += 1;
3960
+ if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
3961
+ } else {
3962
+ callees.set(key, { name: effectiveName, bindingId: match.bindingId, count: 1,
3963
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
3964
+ }
3965
+ }
3966
+ continue;
3967
+ }
3968
+ if (moduleRoute.external) {
3969
+ noteSite(siteId, 'external', null, call);
3970
+ } else {
3971
+ noteUnverified(siteId, call, 'no-import-link');
3972
+ }
3973
+ continue;
3974
+ }
3975
+
3976
+ // Bare imported-name ownership: `import { process } from './x';
3977
+ // process()` (or Python from-import) denotes the imported export,
3978
+ // not a same-named class method in the caller file. Resolve the
3979
+ // name through the same #217 export-chain discipline used for
3980
+ // module receivers. Only intercept when an explicit binding of
3981
+ // this name exists; ordinary locals continue to lexical binding.
3982
+ if (!call.isMethod && !call.receiver &&
3983
+ langTraits(language)?.typeSystem === 'structural') {
3984
+ const importRoute = _calleeStructuralImportedNameRoute(index, fileEntry, call, language);
3985
+ if (importRoute) {
3986
+ if (importRoute.matches?.length) {
3987
+ for (const match of importRoute.matches) {
3988
+ const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
3989
+ const existing = callees.get(key);
3990
+ if (existing) {
3991
+ existing.count += 1;
3992
+ if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
3993
+ } else {
3994
+ callees.set(key, { name: effectiveName, bindingId: match.bindingId, count: 1,
3995
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
3996
+ }
3997
+ }
3998
+ } else if (importRoute.external) {
3999
+ noteSite(siteId, 'external', null, call);
4000
+ } else {
4001
+ noteUnverified(siteId, call, 'ambiguous-binding');
4002
+ }
4003
+ continue;
4004
+ }
4005
+ }
4006
+
3466
4007
  // Resolve binding within this file (without mutating cached call objects)
3467
4008
  let calleeKey = call.bindingId || effectiveName;
3468
4009
  let bindingResolved = call.bindingId;
3469
4010
  let isUncertain = call.uncertain;
3470
4011
  let uncertainReason = null; // account-mode reason for the unverified bucket
3471
4012
  if (!call.bindingId && fileEntry?.bindings) {
3472
- let bindings = fileEntry.bindings.filter(b => b.name === call.name);
4013
+ // A method terminal resolves through its receiver in every
4014
+ // supported language, not through a bare file/package binding.
4015
+ // This includes deep/chained structural receivers and nominal
4016
+ // calls such as Go `v.ReadConfig()` beside a package function
4017
+ // also named ReadConfig. Otherwise the bare wrapper steals the
4018
+ // exact callee before receiver evidence is considered.
4019
+ const receiverBlindMethodBinding = call.isMethod &&
4020
+ !['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver);
4021
+ let bindings = receiverBlindMethodBinding ? [] :
4022
+ fileEntry.bindings.filter(b => b.name === call.name);
3473
4023
  // For Go, also check sibling files in same directory (same
3474
4024
  // package scope). dirToFiles is rebuilt on every build and
3475
4025
  // cache load in canonical path order — the same iteration
3476
4026
  // order as the full index.files scan it replaces (this loop
3477
4027
  // runs per zero-binding call record, so the full scan was
3478
4028
  // quadratic on large repos — 1037-file grpc-go measured).
3479
- if (bindings.length === 0 && langTraits(language)?.packageScope === 'directory') {
4029
+ if (bindings.length === 0 && !receiverBlindMethodBinding &&
4030
+ langTraits(language)?.packageScope === 'directory') {
3480
4031
  const dir = path.dirname(def.file);
3481
4032
  for (const fp of index.dirToFiles?.get(dir) || []) {
3482
4033
  if (fp === def.file) continue;
@@ -3501,6 +4052,8 @@ function findCallees(index, def, options = {}) {
3501
4052
  noteSite(siteId, 'external', null, call);
3502
4053
  continue;
3503
4054
  } else if (route?.resolve) {
4055
+ const rawType = call.receiverType || localTypes?.get(call.receiver);
4056
+ if (routeVirtualOverride(siteId, call, rawType, route.resolve)) continue;
3504
4057
  bindingResolved = route.resolve.bindingId;
3505
4058
  calleeKey = bindingResolved ||
3506
4059
  `${route.resolve.className}.${effectiveName}`;
@@ -3508,20 +4061,25 @@ function findCallees(index, def, options = {}) {
3508
4061
  isUncertain = true;
3509
4062
  uncertainReason = 'uncertain-receiver';
3510
4063
  } else {
3511
- // JS/TS/Python: mark uncertain unless receiver has import/binding
3512
- // evidence in file scope AND that binding can plausibly have this method.
3513
- // Prevents false positives like m.get() → repository.get() when m is
3514
- // just a parameter, AND dict.get() api.get() when dict is a state object.
3515
- const receiverBinding = call.receiver &&
3516
- fileEntry?.bindings?.find(b => b.name === call.receiver);
3517
- if (!receiverBinding) {
4064
+ // JS/TS/Python: lexical existence of `receiver` proves
4065
+ // only that the variable is in scope, not which class
4066
+ // owns its terminal method. Typed/local-constructor
4067
+ // evidence was already handled by the route above;
4068
+ // module ownership was handled before binding lookup.
4069
+ // Everything left is uncertain. The single-owner rule
4070
+ // below may still promote names with exactly one
4071
+ // project owner, but a multi-owner call such as
4072
+ // `catchall._parse()` must never bind defs[0].
4073
+ const defs = index.symbols.get(call.name) || [];
4074
+ const owners = new Set(defs.filter(d => !NON_CALLABLE_TYPES.has(d.type))
4075
+ .map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
4076
+ .filter(Boolean));
4077
+ if (owners.size > 1) {
3518
4078
  isUncertain = true;
3519
- } else if (receiverBinding.type === 'state') {
3520
- // State objects (module-level dicts/lists) don't have user-defined methods
3521
- isUncertain = true;
3522
- } else if (receiverBinding.type === 'function') {
3523
- // Functions don't have user-defined methods (return value is unknown)
4079
+ uncertainReason = 'method-ambiguous';
4080
+ } else {
3524
4081
  isUncertain = true;
4082
+ uncertainReason = 'uncertain-receiver';
3525
4083
  }
3526
4084
  }
3527
4085
  } else {
@@ -3553,6 +4111,58 @@ function findCallees(index, def, options = {}) {
3553
4111
  }
3554
4112
  }
3555
4113
  }
4114
+ if (bindings.length === 0 && !call.isMethod && !call.isConstructor && def.className &&
4115
+ langTraits(language)?.bareCallReachesMethods) {
4116
+ // A Java bare call in an instance method is implicit-this
4117
+ // even when the defining member is inherited and therefore
4118
+ // absent from this file's lexical bindings. Resolve class
4119
+ // then ancestors; if overrides exist, retain the dispatch
4120
+ // set visibly instead of falling through to defs[0].
4121
+ const callSymbols = index.symbols.get(call.name) || [];
4122
+ const onClass = (cls) => callSymbols.filter(s =>
4123
+ s.className === cls && !NON_CALLABLE_TYPES.has(s.type));
4124
+ let selected = _calleeOverloadSelect(
4125
+ index, call, onClass(def.className), language);
4126
+ if (!selected.match && !selected.ambiguous) {
4127
+ const parents = index._getInheritanceParents?.(def.className, def.file);
4128
+ if (parents) {
4129
+ for (const parentName of parents) {
4130
+ selected = _calleeOverloadSelect(
4131
+ index, call, onClass(parentName), language);
4132
+ if (selected.match || selected.ambiguous) break;
4133
+ }
4134
+ }
4135
+ }
4136
+ if (selected.ambiguous) {
4137
+ noteUnverified(siteId, call, 'overload-ambiguous');
4138
+ continue;
4139
+ }
4140
+ if (selected.match) {
4141
+ if (routeVirtualOverride(siteId, call, def.className, selected.match)) continue;
4142
+ const key = selected.match.bindingId ||
4143
+ `${selected.match.className}.${call.name}`;
4144
+ const existing = callees.get(key);
4145
+ if (existing) {
4146
+ existing.count += 1;
4147
+ if (collectAccount) {
4148
+ existing.sites.push(call.line);
4149
+ existing.siteIds.push(siteId);
4150
+ }
4151
+ } else {
4152
+ callees.set(key, {
4153
+ name: call.name,
4154
+ bindingId: selected.match.bindingId,
4155
+ count: 1,
4156
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }),
4157
+ });
4158
+ }
4159
+ continue;
4160
+ }
4161
+ if (collectAccount) {
4162
+ noteUnverified(siteId, call, 'binding-ambiguous');
4163
+ continue;
4164
+ }
4165
+ }
3556
4166
  if (bindings.length === 1) {
3557
4167
  // For method calls with a receiver, verify the receiver plausibly
3558
4168
  // matches the binding's class. Prevents plt.close() → ReportGenerator.close()
@@ -3573,7 +4183,56 @@ function findCallees(index, def, options = {}) {
3573
4183
  bindingResolved = bindings[0].id;
3574
4184
  calleeKey = bindingResolved;
3575
4185
  } else if (bindings.length > 1) {
3576
- if (call.name === def.name) {
4186
+ if (call.name === def.name && !call.isMethod &&
4187
+ langTraits(language)?.typeSystem === 'structural') {
4188
+ // Lexical recursion in JS/TS/Python binds the
4189
+ // enclosing definition, even when a sibling callback
4190
+ // or test case declares another same-named function.
4191
+ // Selecting "all other bindings" inverted the edge:
4192
+ // Fastify's first nested createNestedRoutes called the
4193
+ // later test's copy instead of itself.
4194
+ const selfBinding = bindings.find(b =>
4195
+ b.id === def.bindingId || b.startLine === def.startLine);
4196
+ if (selfBinding) {
4197
+ bindingResolved = selfBinding.id || def.bindingId;
4198
+ calleeKey = bindingResolved || `${def.file}:${def.startLine}:${def.name}`;
4199
+ } else {
4200
+ isUncertain = true;
4201
+ uncertainReason = 'binding-ambiguous';
4202
+ }
4203
+ } else if (call.name === def.name && !call.isMethod &&
4204
+ def.className && langTraits(language)?.bareCallReachesMethods) {
4205
+ // Java bare calls are implicit-this calls. Even when
4206
+ // the terminal name equals the enclosing overload,
4207
+ // argument shape selects one member of the same-class
4208
+ // family; spraying every overload invents edges and
4209
+ // hides the exact dependency an agent needs.
4210
+ const sameClassSyms = (index.symbols.get(call.name) || [])
4211
+ .filter(s => s.className === def.className &&
4212
+ !NON_CALLABLE_TYPES.has(s.type));
4213
+ const selected = _calleeOverloadSelect(
4214
+ index, call, sameClassSyms, language);
4215
+ if (selected.ambiguous) {
4216
+ isUncertain = true;
4217
+ uncertainReason = 'overload-ambiguous';
4218
+ } else if (selected.match) {
4219
+ if (routeVirtualOverride(siteId, call, def.className, selected.match)) continue;
4220
+ if (selected.match.bindingId === def.bindingId ||
4221
+ selected.match.startLine === def.startLine) {
4222
+ noteSite(siteId, 'excluded', 'self-recursion', call);
4223
+ continue;
4224
+ }
4225
+ const matchingBinding = bindings.find(b =>
4226
+ b.id === selected.match.bindingId ||
4227
+ b.startLine === selected.match.startLine);
4228
+ bindingResolved = matchingBinding?.id || selected.match.bindingId;
4229
+ calleeKey = bindingResolved ||
4230
+ `${def.className}.${call.name}:${selected.match.startLine}`;
4231
+ } else {
4232
+ isUncertain = true;
4233
+ uncertainReason = 'binding-ambiguous';
4234
+ }
4235
+ } else if (call.name === def.name) {
3577
4236
  // Calling same-name function (e.g., Java overloads)
3578
4237
  // Add ALL other overloads as potential callees.
3579
4238
  // A RECEIVER-QUALIFIED same-name call names its type
@@ -3642,8 +4301,31 @@ function findCallees(index, def, options = {}) {
3642
4301
  // Try to resolve to a binding in the same class via symbol lookup
3643
4302
  const callSymbols = index.symbols.get(call.name);
3644
4303
  if (callSymbols) {
3645
- const sameClassSym = callSymbols.find(s => s.className === def.className);
3646
- if (sameClassSym) {
4304
+ const onClass = (cls) => callSymbols.filter(s => s.className === cls);
4305
+ let selected = _calleeOverloadSelect(
4306
+ index, call, onClass(def.className), language);
4307
+ // Java implicit-this lookup includes inherited
4308
+ // members. Resolve the statically selected base
4309
+ // declaration, then let routeVirtualOverride keep
4310
+ // descendant implementations visible rather than
4311
+ // choosing an arbitrary sibling override.
4312
+ if (!selected.match && !selected.ambiguous &&
4313
+ langTraits(language)?.bareCallReachesMethods) {
4314
+ const parents = index._getInheritanceParents?.(def.className, def.file);
4315
+ if (parents) {
4316
+ for (const parentName of parents) {
4317
+ selected = _calleeOverloadSelect(
4318
+ index, call, onClass(parentName), language);
4319
+ if (selected.match || selected.ambiguous) break;
4320
+ }
4321
+ }
4322
+ }
4323
+ if (selected.ambiguous) {
4324
+ isUncertain = true;
4325
+ uncertainReason = 'overload-ambiguous';
4326
+ } else if (selected.match) {
4327
+ const sameClassSym = selected.match;
4328
+ if (routeVirtualOverride(siteId, call, def.className, sameClassSym)) continue;
3647
4329
  // Find the binding that matches this symbol's line
3648
4330
  const matchingBinding = bindings.find(b => b.startLine === sameClassSym.startLine);
3649
4331
  if (matchingBinding) {
@@ -3678,6 +4360,16 @@ function findCallees(index, def, options = {}) {
3678
4360
  }
3679
4361
  }
3680
4362
 
4363
+ // Bare nominal calls inside a class are implicit-this calls. A
4364
+ // lexical binding to a base/interface declaration is not an exact
4365
+ // runtime target when overrides/implementations exist.
4366
+ if (bindingResolved && !call.isMethod && !call.receiver &&
4367
+ langTraits(language)?.bareCallReachesMethods) {
4368
+ const bound = (index.symbols.get(effectiveName) || [])
4369
+ .find(s => s.bindingId === bindingResolved);
4370
+ if (bound && routeVirtualOverride(siteId, call, def.className, bound)) continue;
4371
+ }
4372
+
3681
4373
  // Single project-wide owner (fix #236 — the caller side's
3682
4374
  // #204/#209 rule on the callee side): an untyped-receiver method
3683
4375
  // call whose name has exactly ONE owner type resolves to that
@@ -3844,6 +4536,24 @@ function findCallees(index, def, options = {}) {
3844
4536
 
3845
4537
  if (!match) { noteUnverified(siteId, call, 'inherited-unresolved'); continue; }
3846
4538
 
4539
+ // Virtual self/this dispatch (callee-side parity with the
4540
+ // caller side's strict-ancestor rule): a call lexically in a
4541
+ // base class can execute a descendant override. Confirming
4542
+ // only the base declaration is therefore false precision.
4543
+ // `super.method()` is statically pinned to the parent and is
4544
+ // exempt. In contract mode retain the site as possible
4545
+ // dispatch; legacy command behavior remains unchanged.
4546
+ const overrideOwners = call.receiver !== 'super'
4547
+ ? _descendantOverrideOwners(index, def.className, call.name)
4548
+ : new Set();
4549
+ if (collectAccount && overrideOwners.size > 0) {
4550
+ noteUnverified(siteId, call, 'possible-dispatch', {
4551
+ dispatchVia: def.className,
4552
+ dispatchCandidates: overrideOwners.size + 1,
4553
+ });
4554
+ continue;
4555
+ }
4556
+
3847
4557
  const key = match.bindingId || `${match.className}.${call.name}`;
3848
4558
  const existing = callees.get(key);
3849
4559
  if (existing) {
@@ -3988,8 +4698,10 @@ function findCallees(index, def, options = {}) {
3988
4698
  if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
3989
4699
  const isFuncField = callee.type === 'field' && callee.fieldType &&
3990
4700
  /^func\b/.test(callee.fieldType);
4701
+ const isCallableClass = callee.type === 'class' &&
4702
+ langTraits(language)?.classesCallableWithoutNew;
3991
4703
  // Constructor calls (new Foo()) are always callable regardless of type
3992
- if (!isFuncField && !isConstructor) {
4704
+ if (!isFuncField && !isConstructor && !isCallableClass) {
3993
4705
  claimSites('excluded', 'non-callable-shadow');
3994
4706
  continue;
3995
4707
  }
@@ -4017,6 +4729,8 @@ function findCallees(index, def, options = {}) {
4017
4729
  callCount: count,
4018
4730
  weight: index.calculateWeight(count),
4019
4731
  confidence: calleeScored.confidence,
4732
+ evidenceScore: calleeScored.evidenceScore,
4733
+ scoreKind: calleeScored.scoreKind,
4020
4734
  resolution: calleeScored.resolution,
4021
4735
  ...(collectAccount && {
4022
4736
  tier: TIER.CONFIRMED,
@@ -4050,9 +4764,12 @@ function findCallees(index, def, options = {}) {
4050
4764
 
4051
4765
  return result;
4052
4766
  } catch (e) {
4053
- // Expected: file read/parse failures (minified, binary, buffer exceeded).
4054
- // Return empty callees rather than crashing the entire query.
4055
- return [];
4767
+ // Empty callees is a semantic assertion, not an error fallback. Any
4768
+ // read/cache/parser/engine failure here would otherwise turn a broken
4769
+ // trace-down query into a believable false zero.
4770
+ const rel = def?.file ? path.relative(index.root, def.file) : '<unknown>';
4771
+ e.message = `findCallees failed while analyzing ${rel}: ${e.message}`;
4772
+ throw e;
4056
4773
  }
4057
4774
  } finally { index._endOp(); }
4058
4775
  }
@@ -4221,14 +4938,81 @@ function _typeNameFromReturnAnnotation(text) {
4221
4938
  * consuming call happens to be.
4222
4939
  */
4223
4940
  function _buildReturnTypeFlowMap(index, filePath, calls) {
4941
+ // File-scoped derivation: the result is independent of the queried
4942
+ // definition. A command operation can therefore share it across every
4943
+ // findCallers/findCallees request in that file. Cache null explicitly via
4944
+ // Map.has() so files without resolvable flow are cheap too.
4945
+ const opCache = index._opReturnTypeFlowCache;
4946
+ if (opCache?.has(filePath)) return opCache.get(filePath);
4224
4947
  const fileEntry = index.files.get(filePath);
4225
4948
  const language = fileEntry?.language;
4226
4949
  const nominal = langTraits(language)?.typeSystem === 'nominal';
4227
4950
  let map = null;
4951
+ const assignedFoldCtx = {
4952
+ memo: new Map(),
4953
+ visiting: new Set(),
4954
+ records: calls,
4955
+ getFlowMap: () => map,
4956
+ };
4957
+ // A flow map is nearest-assignment semantics, not "last assignment whose
4958
+ // type UCN happened to understand". Record an explicit tombstone when a
4959
+ // call rebinds a variable but its result type cannot be resolved. Without
4960
+ // this, `let m = make_result(); let m = m.unwrap(); m.method()` retained
4961
+ // the stale Result-like type and excluded compiler-true calls on the
4962
+ // unwrapped value (clap-measured).
4963
+ const invalidateAssignment = (call) => {
4964
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
4965
+ const names = [call.assignedTo, ...(call.assignedTupleRest || [])].filter(Boolean);
4966
+ if (names.length === 0) return;
4967
+ if (!map) map = new Map();
4968
+ for (const name of names) {
4969
+ const key = `${scope}:${name}`;
4970
+ if (!map.has(key)) map.set(key, []);
4971
+ map.get(key).push({ line: call.line, invalidated: true });
4972
+ }
4973
+ };
4974
+ const routeUnknownAssignment = (call, externalVia) => {
4975
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
4976
+ const names = [call.assignedTo, ...(call.assignedTupleRest || [])].filter(Boolean);
4977
+ if (names.length === 0) return;
4978
+ if (!map) map = new Map();
4979
+ for (const name of names) {
4980
+ const key = `${scope}:${name}`;
4981
+ if (!map.has(key)) map.set(key, []);
4982
+ map.get(key).push({ line: call.line, externalVia });
4983
+ }
4984
+ };
4228
4985
  for (const call of calls) {
4229
4986
  if (!call.assignedTo) continue;
4230
- let returnType, fromFile, selfClass;
4231
- if (call.isMethod && call.receiverType) {
4987
+
4988
+ // Assigned builder chain (chi-measured):
4989
+ // `hr := RouteHeaders().Route(...)` stores the OUTERMOST method call
4990
+ // as the assignment producer. Fold the chain to its declared result
4991
+ // before composing later `hr.Handler(...)` flow.
4992
+ if (nominal && call.receiverCall) {
4993
+ const folded = _typeOfCallResultFold(
4994
+ index, fileEntry, filePath, call, assignedFoldCtx);
4995
+ if (folded?.type || folded?.externalVia) {
4996
+ const scope = call.enclosingFunction
4997
+ ? `${call.enclosingFunction.startLine}` : '';
4998
+ const key = `${scope}:${call.assignedTo}`;
4999
+ if (!map) map = new Map();
5000
+ if (!map.has(key)) map.set(key, []);
5001
+ map.get(key).push({ line: call.line,
5002
+ ...(folded.type && { type: folded.type }),
5003
+ ...(folded.fromFile && { fromFile: folded.fromFile }),
5004
+ ...(folded.externalVia && { externalVia: folded.externalVia }) });
5005
+ continue;
5006
+ }
5007
+ }
5008
+ let returnType, fromFile, selfClass, returnedFunctionResult;
5009
+ const callableFlow = !call.isMethod && !call.receiver
5010
+ ? _lookupReturnTypeFlow(map, { ...call, receiver: call.name })
5011
+ : undefined;
5012
+ if (callableFlow?.returnedFunctionResult) {
5013
+ returnType = callableFlow.returnedFunctionResult;
5014
+ fromFile = callableFlow.fromFile;
5015
+ } else if (call.isMethod && call.receiverType) {
4232
5016
  const defs = index.symbols.get(call.name) || [];
4233
5017
  if (nominal) {
4234
5018
  const matches = defs.filter(d => d.className === call.receiverType && d.returnType);
@@ -4241,6 +5025,37 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
4241
5025
  const def = defs.find(d => d.className === call.receiverType && d.returnType);
4242
5026
  returnType = def && def.returnType;
4243
5027
  }
5028
+ } else if (call.isMethod && call.receiver &&
5029
+ !['self', 'this', 'cls'].includes(call.receiver) &&
5030
+ _lookupReturnTypeFlow(map, call)) {
5031
+ // One-hop local flow composition (fix #273, chi-measured):
5032
+ // `hr := RouteHeaders(); handler := hr.Handler(...)` first types
5033
+ // hr from RouteHeaders' annotation, then resolves Handler on that
5034
+ // exact owner to type handler. This stays order/scoped and uses
5035
+ // only compiler-declared returns; disagreement remains untyped.
5036
+ const receiverFlow = _lookupReturnTypeFlow(map, call);
5037
+ if (receiverFlow?.externalVia) {
5038
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
5039
+ const key = `${scope}:${call.assignedTo}`;
5040
+ if (!map) map = new Map();
5041
+ if (!map.has(key)) map.set(key, []);
5042
+ map.get(key).push({
5043
+ line: call.line,
5044
+ externalVia: `${receiverFlow.externalVia}.${call.name}`,
5045
+ });
5046
+ continue;
5047
+ }
5048
+ if (receiverFlow?.type) {
5049
+ const matches = (index.symbols.get(call.name) || []).filter(d =>
5050
+ (d.className === receiverFlow.type ||
5051
+ (d.receiver && d.receiver.replace(/^\*/, '') === receiverFlow.type)) &&
5052
+ d.returnType);
5053
+ if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
5054
+ returnType = matches[0].returnType;
5055
+ fromFile = matches[0].file;
5056
+ selfClass = receiverFlow.type;
5057
+ }
5058
+ }
4244
5059
  } else if (call.isMethod && ['self', 'this', 'cls'].includes(call.receiver)) {
4245
5060
  const enclosing = index.findEnclosingFunction(filePath, call.line, true);
4246
5061
  let cls = enclosing && enclosing.className;
@@ -4350,6 +5165,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
4350
5165
  }
4351
5166
  if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
4352
5167
  returnType = matches[0].returnType;
5168
+ if (new Set(matches.map(d => d.file)).size === 1) fromFile = matches[0].file;
4353
5169
  }
4354
5170
  }
4355
5171
  } else if (!call.isMethod && !call.receiver) {
@@ -4368,23 +5184,91 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
4368
5184
  const sameFile = defs.filter(d => d.file === filePath);
4369
5185
  if (sameFile.length === 1) chosen = sameFile[0];
4370
5186
  }
4371
- if (chosen) { returnType = chosen.returnType; fromFile = chosen.file; }
5187
+ if (chosen) {
5188
+ returnType = chosen.returnType;
5189
+ returnedFunctionResult = chosen.returnedFunctionResult;
5190
+ fromFile = chosen.file;
5191
+ }
5192
+ }
5193
+ if (!returnType) {
5194
+ invalidateAssignment(call);
5195
+ continue;
5196
+ }
5197
+ if (language === 'go' && returnedFunctionResult) {
5198
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
5199
+ const key = `${scope}:${call.assignedTo}`;
5200
+ if (!map) map = new Map();
5201
+ if (!map.has(key)) map.set(key, []);
5202
+ map.get(key).push({
5203
+ line: call.line,
5204
+ returnedFunctionResult,
5205
+ ...(fromFile && { fromFile }),
5206
+ });
5207
+ continue;
4372
5208
  }
4373
- if (!returnType) continue;
4374
5209
  let typeName, entryFromFile;
4375
5210
  if (nominal) {
4376
5211
  const parsed = _returnTypeNameNominal(returnType, language, {
4377
5212
  unwrapped: call.assignedUnwrap, tuple: call.assignedTuple, selfClass,
4378
5213
  });
4379
- if (!parsed) continue;
5214
+ if (!parsed) {
5215
+ const uncertainVia = _rejectedNominalFlowVia(returnType, language, {
5216
+ tuple: call.assignedTuple,
5217
+ });
5218
+ if (uncertainVia) routeUnknownAssignment(call, uncertainVia);
5219
+ else invalidateAssignment(call);
5220
+ continue;
5221
+ }
5222
+ // Qualifier provenance is meaningful even when no same-named
5223
+ // project TYPE symbol exists. `http.Handler` is externally owned
5224
+ // regardless of whether the project happens to define a struct,
5225
+ // interface, or named function type called Handler.
5226
+ if (language === 'go' && parsed.qualifier) {
5227
+ const producerEntry = index.files.get(fromFile || filePath) || fileEntry;
5228
+ const qualified = _goQualifiedReceiverType(
5229
+ index, producerEntry, parsed.qualifier, parsed.name);
5230
+ if (qualified && qualified.kind !== 'project') {
5231
+ const scope = call.enclosingFunction
5232
+ ? `${call.enclosingFunction.startLine}` : '';
5233
+ const key = `${scope}:${call.assignedTo}`;
5234
+ if (!map) map = new Map();
5235
+ if (!map.has(key)) map.set(key, []);
5236
+ map.get(key).push({ line: call.line, externalVia: qualified.via });
5237
+ continue;
5238
+ }
5239
+ }
4380
5240
  const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, parsed.name, parsed.qualifier);
4381
- if (!origin) continue; // identity unpinnable — don't type at all
5241
+ if (!origin) {
5242
+ // A project producer can return a package-qualified external
5243
+ // Go type (`DefaultLogger(...) http.Handler`). That still
5244
+ // gives compiler-grade provenance: the assigned receiver is
5245
+ // externally decided and must defeat project-local bare-name
5246
+ // confirmation. Preserve it on the same visible uncertainty
5247
+ // rail as an external producer call.
5248
+ invalidateAssignment(call);
5249
+ continue; // identity unpinnable — don't type at all
5250
+ }
4382
5251
  typeName = parsed.name;
4383
5252
  entryFromFile = origin.fromFile;
4384
5253
  } else {
4385
5254
  typeName = _typeNameFromReturnAnnotation(returnType);
5255
+ if (typeName) {
5256
+ const typeDefs = (index.symbols.get(typeName) || [])
5257
+ .filter(d => IDENTITY_TYPE_KINDS.has(d.type));
5258
+ if (typeDefs.length > 0) {
5259
+ const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, typeName);
5260
+ if (!origin) {
5261
+ invalidateAssignment(call);
5262
+ continue;
5263
+ }
5264
+ entryFromFile = origin.fromFile;
5265
+ }
5266
+ }
5267
+ }
5268
+ if (!typeName) {
5269
+ invalidateAssignment(call);
5270
+ continue;
4386
5271
  }
4387
- if (!typeName) continue;
4388
5272
  const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
4389
5273
  const key = `${scope}:${call.assignedTo}`;
4390
5274
  if (!map) map = new Map();
@@ -4392,11 +5276,80 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
4392
5276
  map.get(key).push({ line: call.line, type: typeName,
4393
5277
  ...(entryFromFile && { fromFile: entryFromFile }) });
4394
5278
  }
5279
+ if (opCache) opCache.set(filePath, map);
4395
5280
  return map;
4396
5281
  }
4397
5282
 
5283
+ /**
5284
+ * Return call records whose source line falls inside one definition.
5285
+ *
5286
+ * Parser output is normally source-ordered. We verify/order it once per file
5287
+ * per command and then use two binary searches, avoiding a full-file scan for
5288
+ * every symbol during reachability traversal.
5289
+ */
5290
+ function _callsInDefinitionRange(index, filePath, calls, startLine, endLine) {
5291
+ let ordered = index._opCallsByLineCache?.get(filePath);
5292
+ if (!ordered) {
5293
+ let sorted = true;
5294
+ for (let i = 1; i < calls.length; i++) {
5295
+ if ((calls[i - 1].line || 0) > (calls[i].line || 0)) {
5296
+ sorted = false;
5297
+ break;
5298
+ }
5299
+ }
5300
+ ordered = sorted ? calls : [...calls].sort((a, b) => (a.line || 0) - (b.line || 0));
5301
+ index._opCallsByLineCache?.set(filePath, ordered);
5302
+ }
5303
+ const lowerBound = (needle) => {
5304
+ let lo = 0, hi = ordered.length;
5305
+ while (lo < hi) {
5306
+ const mid = (lo + hi) >>> 1;
5307
+ if ((ordered[mid].line || 0) < needle) lo = mid + 1;
5308
+ else hi = mid;
5309
+ }
5310
+ return lo;
5311
+ };
5312
+ return ordered.slice(lowerBound(startLine), lowerBound(endLine + 1));
5313
+ }
5314
+
5315
+ /**
5316
+ * Return independently-addressable class method ranges nested inside one
5317
+ * definition. Reachability calls findCallees for thousands of symbols; a
5318
+ * full fileEntry.symbols scan per symbol made this step quadratic in files
5319
+ * with many methods. Cache one sorted range list per file, then binary-slice
5320
+ * the relevant start-line window for each definition.
5321
+ */
5322
+ function _innerClassMethodRanges(index, def, fileEntry) {
5323
+ let ordered = index._opInnerSymbolRangesCache?.get(def.file);
5324
+ if (!ordered) {
5325
+ ordered = fileEntry.symbols
5326
+ .filter(symbol => !NON_CALLABLE_TYPES.has(symbol.type) && symbol.className)
5327
+ .map(symbol => [symbol.startLine, symbol.endLine])
5328
+ .sort((a, b) => a[0] - b[0] || a[1] - b[1]);
5329
+ index._opInnerSymbolRangesCache?.set(def.file, ordered);
5330
+ }
5331
+
5332
+ const lowerBound = (needle) => {
5333
+ let lo = 0, hi = ordered.length;
5334
+ while (lo < hi) {
5335
+ const mid = (lo + hi) >>> 1;
5336
+ if (ordered[mid][0] < needle) lo = mid + 1;
5337
+ else hi = mid;
5338
+ }
5339
+ return lo;
5340
+ };
5341
+ const from = lowerBound(def.startLine + 1);
5342
+ const to = lowerBound(def.endLine + 1);
5343
+ const ranges = [];
5344
+ for (let i = from; i < to; i++) {
5345
+ if (ordered[i][1] <= def.endLine) ranges.push(ordered[i]);
5346
+ }
5347
+ return ranges;
5348
+ }
5349
+
4398
5350
  /** Nearest preceding flow assignment for this call's receiver (fn scope, then module). */
4399
5351
  function _lookupReturnTypeFlow(map, call) {
5352
+ if (!map) return undefined;
4400
5353
  const fnScope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
4401
5354
  for (const scope of fnScope === '' ? [''] : [fnScope, '']) {
4402
5355
  const entries = map.get(`${scope}:${call.receiver}`);
@@ -4405,7 +5358,7 @@ function _lookupReturnTypeFlow(map, call) {
4405
5358
  for (const e of entries) {
4406
5359
  if (e.line < call.line && (!best || e.line > best.line)) best = e;
4407
5360
  }
4408
- if (best) return best;
5361
+ if (best) return best.invalidated ? undefined : best;
4409
5362
  }
4410
5363
  return undefined;
4411
5364
  }
@@ -4503,6 +5456,36 @@ function _returnTypeNameNominal(text, language, opts = {}) {
4503
5456
  return { name: norm, qualifier };
4504
5457
  }
4505
5458
 
5459
+ /**
5460
+ * Some declared nominal return types are deliberately rejected as concrete
5461
+ * receiver identity because they are dispatch contracts or type parameters.
5462
+ * They are still strong evidence AGAINST the single-owner shortcut: `error`
5463
+ * can call any implementation's Error(), Java Object can reach overrides,
5464
+ * and Rust T may be instantiated by any implementing type. Preserve that as
5465
+ * visible possible-dispatch provenance instead of collapsing to "untyped".
5466
+ */
5467
+ function _rejectedNominalFlowVia(text, language, opts = {}) {
5468
+ if (!text || typeof text !== 'string') return null;
5469
+ let raw = text.trim();
5470
+ if (language === 'go' && raw.startsWith('(')) {
5471
+ if (!opts.tuple) return null;
5472
+ const first = raw.slice(1, -1).split(',')[0]?.trim();
5473
+ raw = first ? first.split(/\s+/).pop() : '';
5474
+ }
5475
+ const norm = _normalizeFieldTypeName(raw, language);
5476
+ if (!norm) return null;
5477
+ if (language === 'go' && _GO_FLOW_REJECT.has(norm)) {
5478
+ return `${norm} — builtin contract`;
5479
+ }
5480
+ if (language === 'java' && _JAVA_FLOW_REJECT.has(norm)) {
5481
+ return `${norm} — builtin contract`;
5482
+ }
5483
+ if (language === 'rust' && /^[A-Z][A-Z0-9]?$/.test(norm)) {
5484
+ return `${norm} — generic type`;
5485
+ }
5486
+ return null;
5487
+ }
5488
+
4506
5489
  /**
4507
5490
  * Pin a flow type name to its defining file from the PRODUCER's scope
4508
5491
  * (fix #207 — the #206 identity lesson applied to annotations: `Builder` in
@@ -4518,28 +5501,35 @@ function _returnTypeNameNominal(text, language, opts = {}) {
4518
5501
  * - no project type def at all: external name — safe, can't conflate
4519
5502
  */
4520
5503
  function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
5504
+ const opCache = index._opFlowTypeOriginCache;
5505
+ const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
5506
+ if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
5507
+ const finish = (result) => {
5508
+ if (opCache) opCache.set(cacheKey, result);
5509
+ return result;
5510
+ };
4521
5511
  const typeDefs = (index.symbols.get(typeName) || [])
4522
5512
  .filter(d => IDENTITY_TYPE_KINDS.has(d.type) && d.file);
4523
- if (typeDefs.length === 0) return { fromFile: producerFile };
4524
- if (qualifier === '<unresolvable>') return null;
5513
+ if (typeDefs.length === 0) return finish({ fromFile: producerFile });
5514
+ if (qualifier === '<unresolvable>') return finish(null);
4525
5515
  if (qualifier) {
4526
5516
  const fe = index.files.get(producerFile);
4527
5517
  const inPkg = fe && _qualifiedProducerDefs(index, fe, qualifier, typeDefs);
4528
5518
  if (inPkg && inPkg.length > 0 && new Set(inPkg.map(d => d.file)).size === 1) {
4529
- return { fromFile: inPkg[0].file };
5519
+ return finish({ fromFile: inPkg[0].file });
4530
5520
  }
4531
- return null;
5521
+ return finish(null);
4532
5522
  }
4533
5523
  const sameFile = typeDefs.find(d => d.file === producerFile);
4534
- if (sameFile) return { fromFile: sameFile.file };
5524
+ if (sameFile) return finish({ fromFile: sameFile.file });
4535
5525
  const dir = path.dirname(producerFile);
4536
5526
  const sameDir = typeDefs.filter(d => path.dirname(d.file) === dir);
4537
- if (sameDir.length === 1) return { fromFile: sameDir[0].file };
4538
- if (sameDir.length > 1) return null;
5527
+ if (sameDir.length === 1) return finish({ fromFile: sameDir[0].file });
5528
+ if (sameDir.length > 1) return finish(null);
4539
5529
  const imports = index.importGraph.get(producerFile);
4540
5530
  if (imports) {
4541
5531
  const imported = typeDefs.filter(d => imports.has(d.file));
4542
- if (imported.length === 1) return { fromFile: imported[0].file };
5532
+ if (imported.length === 1) return finish({ fromFile: imported[0].file });
4543
5533
  }
4544
5534
  // Re-export chains (fix #258, the #209 lesson brought to identity
4545
5535
  // resolution): `use clap::Command` lands the import edge on the crate's
@@ -4547,17 +5537,18 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
4547
5537
  // (depth 4) and pin only when exactly ONE same-name type is reachable.
4548
5538
  const reachable = typeDefs.filter(d =>
4549
5539
  _importReaches(index, producerFile, new Set([d.file])));
4550
- if (reachable.length === 1) return { fromFile: reachable[0].file };
4551
- return null;
5540
+ if (reachable.length === 1) return finish({ fromFile: reachable[0].file });
5541
+ return finish(null);
4552
5542
  }
4553
5543
 
4554
5544
  /**
4555
5545
  * Defs that live in the package an import-qualified receiver names, resolved
4556
5546
  * through the producer file's imports (alias-aware — importNames pairs 1:1
4557
5547
  * with imports for Go). STRICT counterpart of _receiverPackageResolution:
4558
- * used for POSITIVE typing (fix #207), so root-package defs ("." — package
4559
- * identity unverifiable from paths) never match here, while over there they
4560
- * must never be excluded. Returns null when the receiver names no import.
5548
+ * used for POSITIVE typing (fix #207). Prefer the import resolver's concrete
5549
+ * project package directory; this is what makes a subpackage importing its
5550
+ * own module root (`chi.RouteContext`) compiler-grade evidence for root
5551
+ * definitions. Returns null when the receiver names no import.
4561
5552
  */
4562
5553
  function _qualifiedProducerDefs(index, fileEntry, receiver, defs) {
4563
5554
  const modules = fileEntry.imports || [];
@@ -4576,6 +5567,23 @@ function _qualifiedProducerDefs(index, fileEntry, receiver, defs) {
4576
5567
  }) || null;
4577
5568
  }
4578
5569
  if (!importModule || !importModule.includes('/')) return null;
5570
+
5571
+ // graph-build resolves a Go import to one representative source file in
5572
+ // the imported package. Package identity is the containing directory, so
5573
+ // every definition in that directory is eligible. This exact resolver
5574
+ // evidence must precede path heuristics: module-root packages have a
5575
+ // project-relative directory of `.` and were previously (and wrongly)
5576
+ // classified as external producers.
5577
+ const resolvedRel = fileEntry.moduleResolved && fileEntry.moduleResolved[importModule];
5578
+ if (resolvedRel) {
5579
+ const resolvedFile = path.isAbsolute(resolvedRel)
5580
+ ? resolvedRel
5581
+ : path.join(index.root, resolvedRel);
5582
+ const resolvedDir = path.dirname(resolvedFile);
5583
+ const resolved = defs.filter(d => d.file && path.dirname(d.file) === resolvedDir);
5584
+ if (resolved.length > 0) return resolved;
5585
+ }
5586
+
4579
5587
  const parts = importModule.split('/');
4580
5588
  const last = parts[parts.length - 1];
4581
5589
  const pkgSeg = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
@@ -4656,6 +5664,37 @@ function _receiverTypeTrustedForExclusion(index, typeName) {
4656
5664
  return !!defs && defs.some(d => d.type === 'class' || d.type === 'struct');
4657
5665
  }
4658
5666
 
5667
+ /**
5668
+ * Resolve the module that supplied a structural constructor type hint.
5669
+ * Exact project resolution pins later type-identity checks to that module;
5670
+ * an external module or a project-looking resolver gap is explicit routing
5671
+ * evidence, never confirmation evidence.
5672
+ */
5673
+ function _structuralQualifiedReceiverOrigin(index, fileEntry, qualifier, typeName) {
5674
+ const bindings = (fileEntry.importBindings || []).filter(b => b.name === qualifier);
5675
+ if (bindings.length === 0) return { kind: 'unknown', via: `${qualifier}.${typeName}` };
5676
+ let projectish = false;
5677
+ for (const binding of bindings) {
5678
+ const moduleName = String(binding.module || '');
5679
+ const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[moduleName];
5680
+ if (rel) {
5681
+ return {
5682
+ kind: 'project',
5683
+ via: `${qualifier}.${typeName}`,
5684
+ fromFile: path.join(index.root, rel),
5685
+ };
5686
+ }
5687
+ const first = moduleName.split(/[./]/).filter(Boolean)[0];
5688
+ if (moduleName.startsWith('.') ||
5689
+ (first && _projectTopLevelNames(index).has(first))) {
5690
+ projectish = true;
5691
+ }
5692
+ }
5693
+ return projectish
5694
+ ? { kind: 'unknown', via: `${qualifier}.${typeName} — unresolved module` }
5695
+ : { kind: 'external', via: `${qualifier}.${typeName}` };
5696
+ }
5697
+
4659
5698
  /**
4660
5699
  * Resolve which same-name TYPE an unqualified receiver-type name denotes from
4661
5700
  * a caller file's scope, and compare it against the pinned target's package
@@ -4866,6 +5905,19 @@ function _submoduleReceiverModule(index, fileEntry, receiverName) {
4866
5905
  */
4867
5906
  function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
4868
5907
  if (targetFiles.has(fromAbs)) return true;
5908
+ // Import ownership is queried repeatedly while a reachability walk
5909
+ // resolves return types and namespace calls. The graph is immutable for
5910
+ // the duration of an operation, so memoize the bounded BFS by source,
5911
+ // target set, and depth. This removed hundreds of milliseconds of
5912
+ // identical barrel-chain walks on large workspaces.
5913
+ const opCache = index._opImportReachCache;
5914
+ const targetKey = [...targetFiles].sort(codeUnitCompare).join('\x00');
5915
+ const cacheKey = `${maxDepth}\x00${fromAbs}\x00${targetKey}`;
5916
+ if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
5917
+ const finish = (value) => {
5918
+ if (opCache) opCache.set(cacheKey, value);
5919
+ return value;
5920
+ };
4869
5921
  const visited = new Set([fromAbs]);
4870
5922
  let frontier = [fromAbs];
4871
5923
  for (let d = 0; d < maxDepth; d++) {
@@ -4875,7 +5927,7 @@ function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
4875
5927
  if (!edges) continue;
4876
5928
  for (const e of edges) {
4877
5929
  if (visited.has(e)) continue;
4878
- if (targetFiles.has(e)) return true;
5930
+ if (targetFiles.has(e)) return finish(true);
4879
5931
  visited.add(e);
4880
5932
  next.push(e);
4881
5933
  }
@@ -4883,7 +5935,7 @@ function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
4883
5935
  if (next.length === 0) break;
4884
5936
  frontier = next;
4885
5937
  }
4886
- return false;
5938
+ return finish(false);
4887
5939
  }
4888
5940
 
4889
5941
  /**
@@ -5000,8 +6052,8 @@ function _sameNominalPackageDir(dirA, dirB, language) {
5000
6052
  if (dirA === dirB) return true;
5001
6053
  if (language !== 'java') return false;
5002
6054
  const norm = (d) => {
5003
- const m = d.match(/^(.*?)[\/\\]src[\/\\][^\/\\]+[\/\\]java(?:[\/\\](.*))?$/);
5004
- return m ? `${m[1]}${m[2] || ''}` : null;
6055
+ const m = d.match(/^(.*?)[/\\]src[/\\][^/\\]+[/\\]java(?:[/\\](.*))?$/);
6056
+ return m ? `${m[1]}\0${m[2] || ''}` : null;
5005
6057
  };
5006
6058
  const a = norm(dirA);
5007
6059
  if (a === null) return false;
@@ -5011,12 +6063,15 @@ function _sameNominalPackageDir(dirA, dirB, language) {
5011
6063
  function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
5012
6064
  const typeDefs = (index.symbols.get(knownType) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
5013
6065
  if (typeDefs.length <= 1) return 'target';
6066
+ const language = index.files.get(filePath)?.language;
5014
6067
  const targetDirs = new Set(targetDefs.map(d => d.file && path.dirname(d.file)).filter(Boolean));
5015
- const inTargetPkg = (d) => d.file && targetDirs.has(path.dirname(d.file));
6068
+ const inTargetPkg = (d) => d.file && [...targetDirs].some(dir =>
6069
+ _sameNominalPackageDir(path.dirname(d.file), dir, language));
5016
6070
  const sameFile = typeDefs.filter(d => d.file === filePath);
5017
6071
  if (sameFile.length > 0) return sameFile.some(inTargetPkg) ? 'target' : 'other';
5018
6072
  const callerDir = path.dirname(filePath);
5019
- const sameDir = typeDefs.filter(d => d.file && path.dirname(d.file) === callerDir);
6073
+ const sameDir = typeDefs.filter(d => d.file &&
6074
+ _sameNominalPackageDir(path.dirname(d.file), callerDir, language));
5020
6075
  if (sameDir.length > 0) return sameDir.some(inTargetPkg) ? 'target' : 'other';
5021
6076
  const imports = index.importGraph.get(filePath);
5022
6077
  if (imports) {
@@ -5026,6 +6081,71 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
5026
6081
  return 'unknown';
5027
6082
  }
5028
6083
 
6084
+ /**
6085
+ * Resolve a structural flow type by declaration origin and walk that exact
6086
+ * declaration's inheritance chain toward the pinned target owner. This is
6087
+ * stricter than name-only targetTypes and more accurate than comparing the
6088
+ * receiver type's directory directly with the target method's directory:
6089
+ * legitimate subclasses routinely live elsewhere (Click test-local
6090
+ * CustomCommand extends click.Command), while parallel package versions may
6091
+ * reuse every class name (zod v3/v4 ZodArray -> ZodType).
6092
+ */
6093
+ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs) {
6094
+ const origin = _resolveFlowTypeOrigin(index, originFile, knownType);
6095
+ if (!origin?.fromFile) return 'unknown';
6096
+ const targetOwners = new Set(targetDefs
6097
+ .map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
6098
+ .filter(Boolean));
6099
+ if (targetOwners.size === 0) return 'unknown';
6100
+ const targetFiles = new Map();
6101
+ for (const owner of targetOwners) {
6102
+ const methodFiles = targetDefs.filter(d =>
6103
+ (d.className || (d.receiver && d.receiver.replace(/^\*/, ''))) === owner)
6104
+ .map(d => d.file).filter(Boolean);
6105
+ const typeDefs = (index.symbols.get(owner) || [])
6106
+ .filter(d => IDENTITY_TYPE_KINDS.has(d.type) && d.file);
6107
+ const files = new Set();
6108
+ for (const mf of methodFiles) {
6109
+ for (const td of typeDefs) {
6110
+ if (td.file === mf || path.dirname(td.file) === path.dirname(mf)) files.add(td.file);
6111
+ }
6112
+ if (files.size === 0) files.add(mf);
6113
+ }
6114
+ targetFiles.set(owner, files);
6115
+ }
6116
+ const ownerIdentity = (name, file) => {
6117
+ if (!targetOwners.has(name)) return null;
6118
+ if (!file) return 'unknown';
6119
+ const files = targetFiles.get(name) || new Set();
6120
+ if (files.has(file)) return 'target';
6121
+ const sameDir = [...files].some(f => path.dirname(f) === path.dirname(file));
6122
+ return sameDir ? 'target' : 'other';
6123
+ };
6124
+ const direct = ownerIdentity(knownType, origin.fromFile);
6125
+ if (direct) return direct;
6126
+
6127
+ const queue = [{ name: knownType, file: origin.fromFile }];
6128
+ const visited = new Set();
6129
+ let sawUnresolved = false;
6130
+ while (queue.length > 0 && visited.size < 128) {
6131
+ const cur = queue.shift();
6132
+ const key = `${cur.file || ''}\0${cur.name}`;
6133
+ if (visited.has(key)) continue;
6134
+ visited.add(key);
6135
+ const parents = index._getInheritanceParents(cur.name, cur.file) || [];
6136
+ for (const parent of parents) {
6137
+ const pFile = index._resolveClassFile
6138
+ ? index._resolveClassFile(parent, cur.file) : undefined;
6139
+ const verdict = ownerIdentity(parent, pFile);
6140
+ if (verdict === 'target') return 'target';
6141
+ if (verdict === 'unknown') sawUnresolved = true;
6142
+ if (pFile) queue.push({ name: parent, file: pFile });
6143
+ else sawUnresolved = true;
6144
+ }
6145
+ }
6146
+ return sawUnresolved ? 'unknown' : 'other';
6147
+ }
6148
+
5029
6149
  /**
5030
6150
  * Is typeName an ancestor (transitively) of any target definition's class?
5031
6151
  * Used by receiver-class disambiguation: a receiver typed as a SUPERTYPE of
@@ -5079,6 +6199,18 @@ function _collectDescendants(index, className, cap = 256) {
5079
6199
  return out;
5080
6200
  }
5081
6201
 
6202
+ function _descendantOverrideOwners(index, className, methodName) {
6203
+ const descendants = _collectDescendants(index, className);
6204
+ descendants.delete(className);
6205
+ const owners = new Set();
6206
+ for (const d of (index.symbols.get(methodName) || [])) {
6207
+ if (NON_CALLABLE_TYPES.has(d.type)) continue;
6208
+ const owner = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
6209
+ if (owner && descendants.has(owner)) owners.add(owner);
6210
+ }
6211
+ return owners;
6212
+ }
6213
+
5082
6214
  function _shareProjectDescendant(index, className, targetClasses) {
5083
6215
  if (!targetClasses || targetClasses.size === 0) return false;
5084
6216
  const mine = _collectDescendants(index, className);
@@ -5117,7 +6249,7 @@ function _shareProjectDescendant(index, className, targetClasses) {
5117
6249
  * signature member and never closes. `definitions` is the same-name def list,
5118
6250
  * so the group key needs no name component.
5119
6251
  */
5120
- function _closeOverloadGroup(targetDefs, definitions) {
6252
+ function _closeCallableIdentityGroup(targetDefs, definitions) {
5121
6253
  const keyOf = (d) => `${d.file}\0${d.className || ''}\0${d.isNested ? 1 : 0}`;
5122
6254
  const groups = new Map();
5123
6255
  for (const d of definitions) {
@@ -5136,6 +6268,30 @@ function _closeOverloadGroup(targetDefs, definitions) {
5136
6268
  expanded.push(d);
5137
6269
  }
5138
6270
  }
6271
+
6272
+ // TypeScript declaration/runtime pairing (preact-signals measured):
6273
+ // `class Signal { constructor(...) }` supplies the public type surface,
6274
+ // while `function Signal(this: Signal, ...)` is the ES5-size-optimized
6275
+ // runtime constructor. `new Signal()` resolves to both in the compiler
6276
+ // oracle. The explicit this:Self parameter is the proof; ordinary same-
6277
+ // name class/function declarations never close.
6278
+ const explicitThisSelf = (d) => {
6279
+ if (d.type !== 'function' || !Array.isArray(d.paramsStructured)) return false;
6280
+ const first = d.paramsStructured[0];
6281
+ if (!first || first.name !== 'this' || !first.type) return false;
6282
+ const head = String(first.type).replace(/<.*$/, '').split('.').pop();
6283
+ return head === d.name;
6284
+ };
6285
+ for (const t of targetDefs) {
6286
+ const pair = definitions.filter(d => d.file === t.file && d.name === t.name &&
6287
+ ((t.type === 'class' && explicitThisSelf(d)) ||
6288
+ (explicitThisSelf(t) && d.type === 'class')));
6289
+ for (const d of pair) {
6290
+ if (targetDefs.includes(d) || (expanded && expanded.includes(d))) continue;
6291
+ if (!expanded) expanded = [...targetDefs];
6292
+ expanded.push(d);
6293
+ }
6294
+ }
5139
6295
  return expanded || targetDefs;
5140
6296
  }
5141
6297
 
@@ -5196,9 +6352,68 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
5196
6352
  // wins so import "k8s.io/client-go/kubernetes/scheme" prefers a def in
5197
6353
  // .../kubernetes/scheme/ over .../kubeadm/scheme/). Extracted for reuse:
5198
6354
  // the parser marks some package calls isMethod:false (fix #268).
6355
+ function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
6356
+ const bindings = (fileEntry?.importBindings || []).filter(b => b.name === call.receiver);
6357
+ if (bindings.length === 0) return { unknown: true };
6358
+ return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, call.name);
6359
+ }
6360
+
6361
+ function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
6362
+ const lookupName = call.resolvedName || call.name;
6363
+ let bindings = (fileEntry?.importBindings || []).filter(b => b.name === lookupName);
6364
+ if (call.resolvedName && bindings.some(b => b.alias)) {
6365
+ const paired = bindings.filter(b => b.alias === call.name);
6366
+ if (paired.length > 0) bindings = paired;
6367
+ }
6368
+ if (bindings.length === 0) return null;
6369
+ return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName);
6370
+ }
6371
+
6372
+ function _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, exportName) {
6373
+ const candidates = (index.symbols.get(exportName) || []).filter(d =>
6374
+ (!NON_CALLABLE_TYPES.has(d.type) ||
6375
+ (call.isConstructor && d.type === 'class') ||
6376
+ (langTraits(language)?.classesCallableWithoutNew && d.type === 'class')) &&
6377
+ _calleeLanguageCompatible(index, d, language));
6378
+ const matches = new Map();
6379
+ let sawProjectish = false;
6380
+ let sawUnknown = false;
6381
+ for (const binding of bindings) {
6382
+ const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
6383
+ if (!rel) {
6384
+ const mod = String(binding.module || '');
6385
+ const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
6386
+ if (mod.startsWith('.') ||
6387
+ (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
6388
+ sawProjectish = true;
6389
+ sawUnknown = true;
6390
+ }
6391
+ continue;
6392
+ }
6393
+ sawProjectish = true;
6394
+ const moduleFile = path.join(index.root, rel);
6395
+ for (const d of candidates) {
6396
+ const verdict = _nameBindingReaches(index, moduleFile, exportName, new Set([d.file]));
6397
+ if (verdict === 'yes') matches.set(`${d.file}:${d.startLine}`, d);
6398
+ else if (verdict === 'unknown') sawUnknown = true;
6399
+ }
6400
+ }
6401
+ if (matches.size > 0) return { matches: [...matches.values()] };
6402
+ if (!sawProjectish && !sawUnknown) return { external: true };
6403
+ return { unknown: true };
6404
+ }
6405
+
5199
6406
  function _calleeGoPackageMatch(index, call, importModule) {
5200
- const symbols = index.symbols.get(call.name);
5201
- if (!symbols) return null;
6407
+ const allSymbols = index.symbols.get(call.name);
6408
+ if (!allSymbols) return null;
6409
+ // Package-qualified composite literals (`clients.Locality{}`) name a
6410
+ // TYPE, never a same-named struct field. Ordinary package calls name
6411
+ // callable symbols. Shape-filter before package ranking so the first
6412
+ // declaration in the right directory cannot steal another namespace.
6413
+ const symbols = allSymbols.filter(s => call.isConstructor
6414
+ ? ['struct', 'class', 'type'].includes(s.type)
6415
+ : !NON_CALLABLE_TYPES.has(s.type));
6416
+ if (symbols.length === 0) return null;
5202
6417
  // Self-module imports (fix #268, cobra-measured — the #220(8) go.mod
5203
6418
  // identity): `import "github.com/spf13/cobra"` from doc/ names the
5204
6419
  // module's ROOT package, whose relative dir is '.' and can never
@@ -5236,12 +6451,28 @@ function _calleeGoPackageMatch(index, call, importModule) {
5236
6451
  }
5237
6452
 
5238
6453
  function _calleeOverloadSelect(index, call, matches, language) {
5239
- if (matches.length <= 1) return { match: matches[0] || null };
5240
- if (call.argCount == null || call.argSpread) return { ambiguous: true };
6454
+ if (matches.length === 0) return { match: null };
6455
+ if (call.argCount == null || call.argSpread) {
6456
+ return matches.length === 1 ? { match: matches[0] } : { ambiguous: true };
6457
+ }
5241
6458
  const fits = matches.filter(d => _callArityCompatible(call, [d], language));
5242
- if (fits.length === 1) return { match: fits[0] };
5243
6459
  if (fits.length === 0) return { match: null }; // fits nothing we model — no claim
5244
- const applicable = fits.filter(d => _overloadApplicable(index, call, d));
6460
+ let applicable = language === 'java'
6461
+ ? fits.filter(d => _overloadApplicable(index, call, d))
6462
+ : fits;
6463
+ // Java overload resolution considers fixed-arity applicability before
6464
+ // variable-arity invocation. An inherited fixed method therefore wins
6465
+ // over a subclass varargs overload when the call omits the vararg array.
6466
+ if (language === 'java') {
6467
+ const fixed = applicable.filter(d =>
6468
+ !Array.isArray(d.paramsStructured) ||
6469
+ !d.paramsStructured.some(p => p?.rest));
6470
+ if (fixed.length > 0) applicable = fixed;
6471
+ if (applicable.length > 1) {
6472
+ const mostSpecific = _javaMostSpecificOverload(index, applicable, call.argCount);
6473
+ if (mostSpecific) return { match: mostSpecific };
6474
+ }
6475
+ }
5245
6476
  if (applicable.length === 1) return { match: applicable[0] };
5246
6477
  return { ambiguous: true };
5247
6478
  }
@@ -5440,6 +6671,18 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
5440
6671
 
5441
6672
  const typeKindsOf = (name) => (index.symbols.get(name) || [])
5442
6673
  .filter(d => IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf));
6674
+ const structuralTypeInScope = (name, defs) => {
6675
+ if (defs.some(d => d.file === def.file)) return true;
6676
+ if ((fileEntry?.bindings || []).some(b => b.name === name)) return true;
6677
+ for (const im of (fileEntry?.importBindings || [])) {
6678
+ if (im.name !== name) continue;
6679
+ const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[String(im.module || '')];
6680
+ if (!rel) continue;
6681
+ const abs = path.join(index.root, rel);
6682
+ if (defs.some(d => d.file === abs)) return true;
6683
+ }
6684
+ return false;
6685
+ };
5443
6686
 
5444
6687
  // Multi-segment path receivers (std::sync::Arc::new): the LAST segment
5445
6688
  // is the type; the qualifier owns it (#206). A crate-internal qualifier
@@ -5461,8 +6704,13 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
5461
6704
  return { unverified: 'method-ambiguous' };
5462
6705
  }
5463
6706
  }
5464
- if (!/^[A-Z]/.test(receiver)) return null;
5465
6707
  let typeDefs = typeKindsOf(receiver);
6708
+ // Python permits lowercase class names. Capitalization is only a naming
6709
+ // convention there; an imported/same-scope class binding is stronger AST
6710
+ // evidence that `codes.is_informational()` is type-qualified dispatch.
6711
+ if (!/^[A-Z]/.test(receiver) &&
6712
+ !(traits?.typeSystem === 'structural' && typeDefs.length > 0 &&
6713
+ structuralTypeInScope(receiver, typeDefs))) return null;
5466
6714
  if (typeDefs.length === 0) {
5467
6715
  for (const im of (fileEntry?.importBindings || [])) {
5468
6716
  if (im.name !== receiver) continue;
@@ -5497,9 +6745,7 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
5497
6745
  return null;
5498
6746
  }
5499
6747
  if (traits?.typeSystem === 'structural') {
5500
- const inFile = typeDefs.some(d => d.file === def.file);
5501
- const bound = (fileEntry?.bindings || []).some(b => b.name === receiver);
5502
- if (!inFile && !bound) return null;
6748
+ if (!structuralTypeInScope(receiver, typeDefs)) return null;
5503
6749
  }
5504
6750
  // Alias closure (#208): a pure alias set agreeing on one base is the
5505
6751
  // SAME type — the method may live on the base's inherent impl.
@@ -5511,12 +6757,48 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
5511
6757
  const symbols = index.symbols.get(call.name) || [];
5512
6758
  const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
5513
6759
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
5514
- const matchOn = (tn) => symbols.find(s => isCallable(s) &&
5515
- (s.className === tn || (s.receiver && s.receiver.replace(/^\*/, '') === tn)));
6760
+ if (language === 'java') {
6761
+ // A class-qualified Java call sees static overloads declared on the
6762
+ // qualifier and inherited from its ancestors. Resolve the complete
6763
+ // family in one pass so a fixed inherited method beats an otherwise
6764
+ // applicable subclass varargs overload (JLS phase ordering).
6765
+ const family = [];
6766
+ const seenTypes = new Set();
6767
+ const queue = [...candidateTypes];
6768
+ while (queue.length > 0 && seenTypes.size < 64) {
6769
+ const typeName = queue.shift();
6770
+ if (!typeName || seenTypes.has(typeName)) continue;
6771
+ seenTypes.add(typeName);
6772
+ family.push(...symbols.filter(s => isCallable(s) && s.type === 'static' &&
6773
+ s.className === typeName));
6774
+ const parents = index._getInheritanceParents?.(typeName, def.file);
6775
+ if (parents) queue.push(...parents);
6776
+ }
6777
+ const selected = _calleeOverloadSelect(index, call, family, language);
6778
+ if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
6779
+ if (selected.match) {
6780
+ return { match: selected.match, typeName: selected.match.className || receiver };
6781
+ }
6782
+ // The qualifier still proves ownership when no overload accepts the
6783
+ // argument count. Caller-side verify must see this broken call site as
6784
+ // a mismatch; callee dependency analysis keeps it unverified.
6785
+ if (family.length > 0 && call.argCount != null && !call.argSpread &&
6786
+ family.every(d => !_callArityCompatible(call, [d], language))) {
6787
+ return {
6788
+ unverified: 'arity-mismatch',
6789
+ wrongArityOwner: true,
6790
+ typeName: receiver,
6791
+ };
6792
+ }
6793
+ }
6794
+ const selectOn = (tn) => _calleeOverloadSelect(index, call, symbols.filter(s => isCallable(s) &&
6795
+ (s.className === tn || (s.receiver && s.receiver.replace(/^\*/, '') === tn))), language);
5516
6796
  let match = null;
5517
6797
  let matchedType = null;
5518
6798
  for (const tn of candidateTypes) {
5519
- match = matchOn(tn);
6799
+ const selected = selectOn(tn);
6800
+ if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
6801
+ match = selected.match;
5520
6802
  if (match) { matchedType = tn; break; }
5521
6803
  }
5522
6804
  if (!match && traits?.typeSystem === 'nominal') {
@@ -5524,7 +6806,9 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
5524
6806
  const parentNames = index._getInheritanceParents?.(tn, def.file);
5525
6807
  if (!parentNames) continue;
5526
6808
  for (const pName of parentNames) {
5527
- match = matchOn(pName);
6809
+ const selected = selectOn(pName);
6810
+ if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
6811
+ match = selected.match;
5528
6812
  if (match) { matchedType = pName; break; }
5529
6813
  }
5530
6814
  if (match) break;
@@ -5602,8 +6886,8 @@ function _receiverTypeAncestors(index, typeName, maxHops = 6) {
5602
6886
  function _calleeReceiverTypeRoute(index, call, localTypes, language) {
5603
6887
  const raw = call.receiverType || localTypes?.get(call.receiver);
5604
6888
  if (!raw || typeof raw !== 'string') return null;
5605
- const head = _structuralTypeHead(raw) || raw;
5606
- const norm = _PY_TYPING_BUILTINS[head] || head;
6889
+ const head = _structuralTypeHead(raw, { index, language }) || raw;
6890
+ const norm = head;
5607
6891
  const defs = (index.symbols.get(call.name) || []).filter(d =>
5608
6892
  !NON_CALLABLE_TYPES.has(d.type) && d.className &&
5609
6893
  _calleeLanguageCompatible(index, d, language));
@@ -5649,6 +6933,7 @@ function _calleeZeroCandidateName(index, call) {
5649
6933
  function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, flowEntry) {
5650
6934
  if (JS_GLOBAL_RECEIVERS.has(call.receiver)) return null;
5651
6935
  if (call.receiverIsModule) return null;
6936
+ if (_universalMethodName(language, name)) return null;
5652
6937
  const traits = langTraits(language);
5653
6938
  if (traits?.typeSystem === 'structural' &&
5654
6939
  _submoduleReceiverModule(index, fileEntry, call.receiver)) return null;
@@ -5681,8 +6966,8 @@ function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, fl
5681
6966
  // a `new Map()` local can never single-owner-confirm a project method).
5682
6967
  if (call.receiverType && typeof call.receiverType === 'string') {
5683
6968
  const owner = ownerKeys.values().next().value;
5684
- const head = _structuralTypeHead(call.receiverType) || call.receiverType;
5685
- const norm = _PY_TYPING_BUILTINS[head] || head;
6969
+ const head = _structuralTypeHead(call.receiverType, { index, language }) || call.receiverType;
6970
+ const norm = head;
5686
6971
  if (head !== owner && norm !== owner &&
5687
6972
  !_receiverTypeAncestors(index, head).has(owner)) return null;
5688
6973
  }
@@ -5744,6 +7029,10 @@ function _callArityCompatible(call, targetDefs, language) {
5744
7029
  }
5745
7030
 
5746
7031
  const JAVA_PRIMITIVES = new Set(['int', 'long', 'short', 'byte', 'char', 'float', 'double', 'boolean']);
7032
+ const JAVA_FINAL_REFERENCE_TYPES = new Set([
7033
+ 'String', 'Class', 'Boolean', 'Byte', 'Character', 'Short',
7034
+ 'Integer', 'Long', 'Float', 'Double', 'Void',
7035
+ ]);
5747
7036
 
5748
7037
  // Which parameter types a call-site literal kind can bind (Java overload
5749
7038
  // resolution: identity, widening, boxing — plus the boxed types' interfaces).
@@ -5772,9 +7061,30 @@ function _javaArgKindMatches(index, kind, paramType) {
5772
7061
  if (!bare || bare === 'Object') return true;
5773
7062
  if (/^[A-Z][0-9]?$/.test(bare)) return true; // generic type variable (T, E, K1...)
5774
7063
  if (kind === 'null') return !JAVA_PRIMITIVES.has(bare);
5775
- if (kind.startsWith('new:') || kind.startsWith('cast:')) {
7064
+ if (kind.startsWith('class:')) {
7065
+ return ['Class', 'Type', 'Serializable', 'GenericDeclaration',
7066
+ 'AnnotatedElement'].includes(bare);
7067
+ }
7068
+ if (kind.startsWith('call:')) {
7069
+ const parts = kind.split(':');
7070
+ const owner = parts[1];
7071
+ const method = parts.slice(2).join(':');
7072
+ const returns = new Set((index.symbols.get(method) || [])
7073
+ .filter(d => d.className === owner && d.returnType)
7074
+ .map(d => String(d.returnType).replace(/<.*$/s, '').trim()
7075
+ .replace(/\[\]$/, '').split('.').pop())
7076
+ .filter(Boolean));
7077
+ // Only an owner-scoped, agreeing return annotation is type evidence.
7078
+ if (returns.size !== 1) return true;
7079
+ return _javaArgKindMatches(index, `type:${[...returns][0]}`, paramType);
7080
+ }
7081
+ if (kind.startsWith('new:') || kind.startsWith('cast:') || kind.startsWith('type:')) {
5776
7082
  const t = kind.slice(kind.indexOf(':') + 1);
5777
7083
  if (t === bare) return true;
7084
+ // These java.lang types are final. A statically known different type
7085
+ // can never bind their overload, even when the argument type's own
7086
+ // ancestry ends in external Object and is therefore incomplete.
7087
+ if (JAVA_FINAL_REFERENCE_TYPES.has(bare)) return false;
5778
7088
  const tDefs = (index.symbols.get(t) || [])
5779
7089
  .filter(d => d.type === 'class' || d.type === 'interface');
5780
7090
  if (tDefs.length === 0) return true; // external arg type — unknowable
@@ -5807,6 +7117,58 @@ function _overloadApplicable(index, call, def) {
5807
7117
  return true;
5808
7118
  }
5809
7119
 
7120
+ function _javaBareParamType(param) {
7121
+ if (!param?.type) return null;
7122
+ return String(param.type).replace(/<.*$/s, '').trim()
7123
+ .replace(/\.\.\.$/, '').replace(/\[\]$/, '').split('.').pop() || null;
7124
+ }
7125
+
7126
+ function _javaParamAt(def, position) {
7127
+ const ps = def.paramsStructured;
7128
+ if (!Array.isArray(ps) || ps.length === 0) return null;
7129
+ if (position < ps.length) return ps[position];
7130
+ const last = ps[ps.length - 1];
7131
+ return last?.rest ? last : null;
7132
+ }
7133
+
7134
+ function _javaTypeAtLeastAsSpecific(index, subType, superType, subDef) {
7135
+ if (!subType || !superType) return false;
7136
+ if (subType === superType) return true;
7137
+ if (superType === 'Object' && !JAVA_PRIMITIVES.has(subType)) return true;
7138
+ if (subType === 'String' && ['CharSequence', 'Comparable', 'Serializable'].includes(superType)) {
7139
+ return true;
7140
+ }
7141
+ if (['Byte', 'Short', 'Integer', 'Long', 'Float', 'Double'].includes(subType) &&
7142
+ superType === 'Number') return true;
7143
+ const subDefs = (index.symbols.get(subType) || [])
7144
+ .filter(d => d.type === 'class' || d.type === 'interface');
7145
+ if (subDefs.length === 0) return false;
7146
+ return _isDispatchAncestor(index, superType, [{
7147
+ className: subType,
7148
+ file: subDef?.file || subDefs[0].file,
7149
+ }]);
7150
+ }
7151
+
7152
+ // Conservative subset of Java's "most specific" rule. A candidate wins only
7153
+ // when every argument position is at least as specific as every competing
7154
+ // candidate and at least one position is strictly more specific. Unknown
7155
+ // relationships stay ambiguous.
7156
+ function _javaMostSpecificOverload(index, applicable, argCount) {
7157
+ if (!Number.isInteger(argCount) || applicable.length < 2) return null;
7158
+ const dominates = (a, b) => {
7159
+ let strict = false;
7160
+ for (let i = 0; i < argCount; i++) {
7161
+ const at = _javaBareParamType(_javaParamAt(a, i));
7162
+ const bt = _javaBareParamType(_javaParamAt(b, i));
7163
+ if (!at || !bt || !_javaTypeAtLeastAsSpecific(index, at, bt, a)) return false;
7164
+ if (at !== bt) strict = true;
7165
+ }
7166
+ return strict;
7167
+ };
7168
+ const winners = applicable.filter(a => applicable.every(b => a === b || dominates(a, b)));
7169
+ return winners.length === 1 ? winners[0] : null;
7170
+ }
7171
+
5810
7172
  /**
5811
7173
  * Overload discipline (Java): when the pinned target has same-class sibling
5812
7174
  * overloads, decide what the call site's static argument shape proves.
@@ -5817,8 +7179,12 @@ function _overloadApplicable(index, call, def) {
5817
7179
  function _overloadDiscipline(index, call, targetDefs, definitions) {
5818
7180
  const targetOwners = new Set(targetDefs.map(d => d.className).filter(Boolean));
5819
7181
  if (targetOwners.size === 0) return null;
7182
+ const targetLanguage = targetDefs[0]?.file && index.files.get(targetDefs[0].file)?.language;
7183
+ const sameOwnerIdentity = (d) => targetLanguage !== 'java' || targetDefs.some(t =>
7184
+ t.className === d.className && t.file && d.file &&
7185
+ _sameNominalPackageDir(path.dirname(t.file), path.dirname(d.file), 'java'));
5820
7186
  const family = definitions.filter(d => !NON_CALLABLE_TYPES.has(d.type) &&
5821
- d.className && targetOwners.has(d.className));
7187
+ d.className && targetOwners.has(d.className) && sameOwnerIdentity(d));
5822
7188
  // Inherited sibling overloads (fix #268, javapoet-measured): the pin's
5823
7189
  // dispatch surface includes ancestor same-name methods — ClassName's
5824
7190
  // annotated(List) coexists with TypeName's FINAL annotated(Spec...), and
@@ -5856,6 +7222,11 @@ function _overloadDiscipline(index, call, targetDefs, definitions) {
5856
7222
  if (family.every(d => pinnedKeys.has(`${d.file}:${d.startLine}`))) return null;
5857
7223
  const applicable = family.filter(d => _overloadApplicable(index, call, d));
5858
7224
  if (applicable.length === 0) return null; // shape fits nothing we model — no claim
7225
+ const mostSpecific = _javaMostSpecificOverload(index, applicable, call.argCount);
7226
+ if (mostSpecific) {
7227
+ return pinnedKeys.has(`${mostSpecific.file}:${mostSpecific.startLine}`)
7228
+ ? null : 'other-overload';
7229
+ }
5859
7230
  const pinnedApplicable = applicable.some(d => pinnedKeys.has(`${d.file}:${d.startLine}`));
5860
7231
  if (!pinnedApplicable) return 'other-overload';
5861
7232
  if (applicable.length === 1) return null; // uniquely the pinned overload
@@ -5957,12 +7328,74 @@ function _dispatchCapableSupertype(index, language, typeName, targetDefs, defini
5957
7328
  if (definitions.some(d => d.className === typeName)) return true;
5958
7329
  return _isDispatchAncestor(index, typeName, targetDefs);
5959
7330
  }
7331
+ // A Go type declared in an external package may be an interface. Since
7332
+ // interface satisfaction is implicit, UCN has no implements edge that
7333
+ // could disprove dispatch into a project method. Route it visibly; never
7334
+ // use this uncertainty to confirm an edge.
7335
+ if (language === 'go' && _externalGoDispatchType(index, typeName) &&
7336
+ targetDefs.some(d => d.className || d.receiver)) return true;
5960
7337
  if (traits.allMethodsVirtual) {
5961
7338
  return _isDispatchAncestor(index, typeName, targetDefs);
5962
7339
  }
5963
7340
  return false;
5964
7341
  }
5965
7342
 
7343
+ function _externalGoDispatchType(index, typeName) {
7344
+ if (!typeName || BUILTIN_RECEIVER_TYPES.has(typeName)) return false;
7345
+ // `error` and `any` are builtin interfaces and therefore dispatch
7346
+ // contracts; other lowercase builtins are concrete and excluded above.
7347
+ if (typeName === 'error' || typeName === 'any') return true;
7348
+ const defs = index.symbols.get(typeName) || [];
7349
+ return !defs.some(d => d.type === 'class' || d.type === 'struct' ||
7350
+ d.type === 'interface' || d.type === 'trait' || d.type === 'type');
7351
+ }
7352
+
7353
+ /**
7354
+ * Resolve the package provenance retained on a Go receiver annotation.
7355
+ * `http.Handler` must never collapse into an unrelated project-local
7356
+ * `Handler`. A resolved project import returns the exact package directory
7357
+ * and its same-named type declarations; an unresolved imported package is
7358
+ * external/opaque and therefore dispatch-capable but never confirmable.
7359
+ */
7360
+ function _goQualifiedReceiverType(index, fileEntry, qualifier, typeName) {
7361
+ if (!fileEntry || !qualifier || !typeName) return null;
7362
+ const modules = fileEntry.imports || [];
7363
+ const names = fileEntry.importNames || [];
7364
+ let importModule = null;
7365
+ if (names.length === modules.length) {
7366
+ const i = names.indexOf(qualifier);
7367
+ if (i >= 0) importModule = modules[i];
7368
+ }
7369
+ if (!importModule) {
7370
+ importModule = modules.find(mod => {
7371
+ const parts = String(mod).split('/');
7372
+ const last = parts[parts.length - 1];
7373
+ const packageName = /^v\d+$/.test(last) && parts.length > 1
7374
+ ? parts[parts.length - 2] : last;
7375
+ return packageName === qualifier;
7376
+ }) || null;
7377
+ }
7378
+ if (!importModule) return null;
7379
+
7380
+ const resolvedRel = fileEntry.moduleResolved && fileEntry.moduleResolved[importModule];
7381
+ if (!resolvedRel) {
7382
+ return { kind: 'opaque', via: `${qualifier}.${typeName}`, importModule };
7383
+ }
7384
+ const resolvedFile = path.isAbsolute(resolvedRel)
7385
+ ? resolvedRel : path.join(index.root, resolvedRel);
7386
+ const dir = path.dirname(resolvedFile);
7387
+ const defs = (index.symbols.get(typeName) || []).filter(d =>
7388
+ d.file && path.dirname(d.file) === dir && IDENTITY_TYPE_KINDS.has(d.type));
7389
+ return { kind: 'project', via: `${qualifier}.${typeName}`, importModule, dir, defs };
7390
+ }
7391
+
7392
+ /** A declared interface/trait receiver names a dispatch contract, not one executable body. */
7393
+ function _isDispatchContractType(index, typeName) {
7394
+ if (!typeName) return false;
7395
+ return (index.symbols.get(typeName) || [])
7396
+ .some(d => d.type === 'interface' || d.type === 'trait');
7397
+ }
7398
+
5966
7399
  /**
5967
7400
  * Like _isAncestorOfTargetClass, but walks `implements` records (Java
5968
7401
  * implements clauses, Rust `impl Trait for Type` surfaced as implements)
@@ -6205,7 +7638,7 @@ function _normalizeFieldTypeName(raw, language) {
6205
7638
  if (langTraits(language)?.typeSystem === 'structural') {
6206
7639
  // JS/TS/Python (fix #219): compiler-true annotation heads, value-
6207
7640
  // position semantics — a field declared Promise<X> HOLDS a Promise.
6208
- return _structuralTypeHead(t);
7641
+ return _structuralTypeHead(t, { language });
6209
7642
  }
6210
7643
  return null;
6211
7644
  }
@@ -6231,6 +7664,7 @@ const _PY_TYPING_BUILTINS = {
6231
7664
  function _structuralTypeHead(text, opts = {}) {
6232
7665
  if (!text || typeof text !== 'string') return null;
6233
7666
  let t = text.trim().replace(/^readonly\s+/, '').replace(/^["']|["']$/g, '').trim();
7667
+ const explicitlyTypingQualified = /^(?:typing\.)/.test(t);
6234
7668
  if (t.includes('|')) {
6235
7669
  const parts = t.split('|').map(s => s.trim())
6236
7670
  .filter(s => s && !['None', 'null', 'undefined'].includes(s));
@@ -6255,7 +7689,28 @@ function _structuralTypeHead(text, opts = {}) {
6255
7689
  if (m) t = m[1];
6256
7690
  const last = t.split('.').pop();
6257
7691
  if (!/^[A-Za-z_$][\w$]*$/.test(last)) return null; // fn types, object literals, tuples
6258
- return _PY_TYPING_BUILTINS[last] || last;
7692
+ // Python's typing aliases share ordinary identifier names with perfectly
7693
+ // valid project classes (`class Text` in rich; user-defined List/Set are
7694
+ // also legal). A global Text -> str rewrite is therefore exclusion-grade
7695
+ // misinformation. Explicit `typing.Text` is unambiguous; an unqualified
7696
+ // alias is normalized only when no project type with that name is
7697
+ // reachable from the annotation's defining file. TypeScript must never
7698
+ // receive Python alias normalization at all.
7699
+ const builtinAlias = _PY_TYPING_BUILTINS[last];
7700
+ if (!builtinAlias || (opts.language && opts.language !== 'python')) return last;
7701
+ if (explicitlyTypingQualified) return builtinAlias;
7702
+ if (opts.index) {
7703
+ const projectTypeDefs = (opts.index.symbols.get(last) || []).filter(d =>
7704
+ ['class', 'interface', 'type', 'trait', 'struct', 'record'].includes(d.type));
7705
+ if (projectTypeDefs.length > 0) {
7706
+ if (!opts.originFile) {
7707
+ if (projectTypeDefs.length === 1) return last;
7708
+ } else if (_resolveFlowTypeOrigin(opts.index, opts.originFile, last)) {
7709
+ return last;
7710
+ }
7711
+ }
7712
+ }
7713
+ return builtinAlias;
6259
7714
  }
6260
7715
 
6261
7716
  // Structural annotation heads that carry no receiver identity: TS escape
@@ -6293,8 +7748,7 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
6293
7748
  const language = fileEntry.language;
6294
7749
  const defs = (index.symbols.get(call.receiverCall) || [])
6295
7750
  .filter(d => !NON_CALLABLE_TYPES.has(d.type));
6296
- let producer = null;
6297
- let selfClass;
7751
+ let producer;
6298
7752
  if (call.receiverCallReceiver) {
6299
7753
  // Package-qualified producer: os.CreateTemp().Name(). A package that
6300
7754
  // resolves to no project def decided the type OUTSIDE the project —
@@ -6308,17 +7762,13 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
6308
7762
  }
6309
7763
  producer = inPkg[0];
6310
7764
  } else if (call.receiverCallIsMethod) {
6311
- // Method producer: every same-name method def project-wide must carry
6312
- // an annotation and agree (#219 discipline whichever class
6313
- // dispatches, the type is the same).
6314
- const methodDefs = defs.filter(d => d.className || d.receiver);
6315
- if (methodDefs.length === 0) return null;
6316
- if (!methodDefs.every(d => d.returnType)) return null;
6317
- if (new Set(methodDefs.map(d => d.returnType)).size !== 1) return null;
6318
- producer = methodDefs[0];
6319
- const classes = new Set(methodDefs.map(d =>
6320
- d.className || (d.receiver || '').replace(/^\*/, '')));
6321
- selfClass = classes.size === 1 ? [...classes][0] : undefined;
7765
+ // An untyped method receiver cannot borrow a return annotation by
7766
+ // terminal name alone. External owners are invisible to the project
7767
+ // index (`Map.Entry.getValue()` vs a unique project getValue()), so
7768
+ // project-wide agreement is not a closed world and is unsafe for
7769
+ // exclusion-grade receiver typing. The recursive fold handles method
7770
+ // producers when their own receiver is typed; otherwise stay unknown.
7771
+ return null;
6322
7772
  } else {
6323
7773
  // Plain producer: Go resolves within the package; others same-file
6324
7774
  // narrowing, then global-unique (#199/#207 rules). Where bare calls
@@ -6354,6 +7804,7 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
6354
7804
  if (!chosen || !chosen.returnType) return null;
6355
7805
  producer = chosen;
6356
7806
  }
7807
+ const selfClass = producer.className || (producer.receiver || '').replace(/^\*/, '') || undefined;
6357
7808
  const parsed = _returnTypeNameNominal(producer.returnType, language, { selfClass });
6358
7809
  if (!parsed) return null;
6359
7810
  const origin = _resolveFlowTypeOrigin(index, producer.file || filePath, parsed.name, parsed.qualifier);
@@ -6376,16 +7827,27 @@ function _chainedReceiverType(index, call, language) {
6376
7827
  if (language === 'python' && !call.receiverCallAwaited &&
6377
7828
  producers.some(d => d.isAsync)) return null;
6378
7829
  const heads = new Set();
7830
+ const origins = new Set();
6379
7831
  for (const d of producers) {
6380
- const h = _structuralTypeHead(d.returnType, { unwrapAsync: call.receiverCallAwaited });
7832
+ const h = _structuralTypeHead(d.returnType, {
7833
+ unwrapAsync: call.receiverCallAwaited, index, language, originFile: d.file,
7834
+ });
6381
7835
  if (!h) return null;
6382
7836
  heads.add(h);
6383
7837
  if (heads.size > 1) return null;
7838
+ const typeDefs = (index.symbols.get(h) || []).filter(td => IDENTITY_TYPE_KINDS.has(td.type));
7839
+ if (typeDefs.length > 0) {
7840
+ const origin = _resolveFlowTypeOrigin(index, d.file, h);
7841
+ if (!origin) return null;
7842
+ origins.add(origin.fromFile);
7843
+ if (origins.size > 1) return null;
7844
+ }
6384
7845
  }
6385
7846
  const head = [...heads][0];
6386
7847
  if (/^[A-Z][A-Z0-9]?$/.test(head)) return null; // generic type param (T, K, V1)
6387
7848
  if (_STRUCTURAL_FLOW_REJECT.has(head)) return null;
6388
- return head;
7849
+ const fromFile = origins.size === 1 ? [...origins][0] : undefined;
7850
+ return { type: head, ...(fromFile && { fromFile }) };
6389
7851
  }
6390
7852
 
6391
7853
  // ── Chained-receiver fold (fix #258, clap-measured) ─────────────────────────
@@ -6469,7 +7931,9 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
6469
7931
  const heads = new Set();
6470
7932
  for (const d of owned) {
6471
7933
  if (!d.returnType) return null;
6472
- let h = _structuralTypeHead(d.returnType, { unwrapAsync: opts.consumerAwaited });
7934
+ let h = _structuralTypeHead(d.returnType, {
7935
+ unwrapAsync: opts.consumerAwaited, index, language, originFile: d.file,
7936
+ });
6473
7937
  if (h === 'this' || h === 'Self') h = selfType;
6474
7938
  if (!h) return null;
6475
7939
  heads.add(h);
@@ -6478,6 +7942,18 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
6478
7942
  const head = [...heads][0];
6479
7943
  if (/^[A-Z][A-Z0-9]?$/.test(head)) return null; // generic type param
6480
7944
  if (_STRUCTURAL_FLOW_REJECT.has(head)) return null;
7945
+ const returnTypeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
7946
+ if (returnTypeDefs.length > 0) {
7947
+ const origins = new Set();
7948
+ for (const d of owned) {
7949
+ const origin = _resolveFlowTypeOrigin(index, d.file || opts.filePath, head);
7950
+ if (!origin) return null;
7951
+ origins.add(origin.fromFile);
7952
+ if (origins.size > 1) return null;
7953
+ }
7954
+ const fromFile = [...origins][0];
7955
+ return { type: head, ...(fromFile && { fromFile }) };
7956
+ }
6481
7957
  return { type: head };
6482
7958
  }
6483
7959
 
@@ -6492,7 +7968,7 @@ function _typeOfCallResultFold(index, fileEntry, filePath, record, ctx, consumer
6492
7968
  if (ctx.memo.has(record)) return ctx.memo.get(record);
6493
7969
  if (ctx.visiting.has(record) || ctx.visiting.size > 64) return null;
6494
7970
  ctx.visiting.add(record);
6495
- let out = null;
7971
+ let out;
6496
7972
  try {
6497
7973
  out = _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited);
6498
7974
  } finally {
@@ -6560,7 +8036,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6560
8036
  }
6561
8037
  // Structural module-qualified producer: z.string() — resolve through the
6562
8038
  // file's import bindings (flow-map rails, incl. the #222 externality test).
6563
- if (!nominal && record.isMethod && record.receiver && record.receiverIsModule) {
8039
+ if (!nominal && record.isMethod && record.receiver &&
8040
+ (record.receiverIsModule || _isStructuralImportReceiver(fileEntry, record))) {
6564
8041
  const binding = (fileEntry?.importBindings || []).find(b => b.name === record.receiver);
6565
8042
  const rel = binding && fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
6566
8043
  if (binding && !rel) {
@@ -6585,14 +8062,27 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6585
8062
  if (language === 'python' && !consumerAwaited && matches.some(d => d.isAsync)) return null;
6586
8063
  const heads = new Set();
6587
8064
  for (const d of matches) {
6588
- const h = _structuralTypeHead(d.returnType, { unwrapAsync: consumerAwaited });
8065
+ const h = _structuralTypeHead(d.returnType, {
8066
+ unwrapAsync: consumerAwaited, index, language, originFile: d.file,
8067
+ });
6589
8068
  if (!h) return null;
6590
8069
  heads.add(h);
6591
8070
  }
6592
8071
  if (heads.size !== 1) return null;
6593
8072
  const head = [...heads][0];
6594
8073
  if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
6595
- return { type: head };
8074
+ const origins = new Set();
8075
+ const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
8076
+ if (typeDefs.length > 0) {
8077
+ for (const d of matches) {
8078
+ const origin = _resolveFlowTypeOrigin(index, d.file || filePath, head);
8079
+ if (!origin) return null;
8080
+ origins.add(origin.fromFile);
8081
+ if (origins.size > 1) return null;
8082
+ }
8083
+ }
8084
+ const fromFile = origins.size === 1 ? [...origins][0] : undefined;
8085
+ return { type: head, ...(fromFile && { fromFile }) };
6596
8086
  }
6597
8087
  // self/this/cls receiver: resolve through the enclosing class (+ walk).
6598
8088
  if (record.isMethod && ['self', 'this', 'cls'].includes(record.receiver)) {
@@ -6626,7 +8116,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6626
8116
  if (!rt && record.receiverCall && (!record.receiver || record.receiverIsChainRoot)) {
6627
8117
  rt = _foldChainedReceiverType(index, fileEntry, filePath, record, ctx);
6628
8118
  }
6629
- if (!rt && record.receiver && !record.receiverIsChainRoot) {
8119
+ if (!rt && record.receiver && !record.receiverIsChainRoot &&
8120
+ !record.receiverPatternShadow && !record.receiverFlowInvalidated) {
6630
8121
  const flowMap = ctx.getFlowMap();
6631
8122
  const fe = flowMap && _lookupReturnTypeFlow(flowMap, record);
6632
8123
  if (fe && fe.externalVia) return { externalVia: fe.externalVia };
@@ -6637,6 +8128,7 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6637
8128
  return _methodReturnOnType(index, rt.type, rt.fromFile, name, language,
6638
8129
  { filePath, consumerAwaited });
6639
8130
  }
8131
+ if (nominal) return null;
6640
8132
  // One-hop agreement (the #207/#219 discipline, one level deeper):
6641
8133
  // every method owner project-wide annotated and agreeing.
6642
8134
  const methodDefs = (index.symbols.get(name) || [])
@@ -6657,14 +8149,27 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6657
8149
  if (language === 'python' && !consumerAwaited && methodDefs.some(d => d.isAsync)) return null;
6658
8150
  const heads = new Set();
6659
8151
  for (const d of methodDefs) {
6660
- const h = _structuralTypeHead(d.returnType, { unwrapAsync: consumerAwaited });
8152
+ const h = _structuralTypeHead(d.returnType, {
8153
+ unwrapAsync: consumerAwaited, index, language, originFile: d.file,
8154
+ });
6661
8155
  if (!h) return null;
6662
8156
  heads.add(h);
6663
8157
  if (heads.size > 1) return null;
6664
8158
  }
6665
8159
  const head = [...heads][0];
6666
8160
  if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
6667
- return { type: head };
8161
+ const origins = new Set();
8162
+ const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
8163
+ if (typeDefs.length > 0) {
8164
+ for (const d of methodDefs) {
8165
+ const origin = _resolveFlowTypeOrigin(index, d.file || filePath, head);
8166
+ if (!origin) return null;
8167
+ origins.add(origin.fromFile);
8168
+ if (origins.size > 1) return null;
8169
+ }
8170
+ }
8171
+ const fromFile = origins.size === 1 ? [...origins][0] : undefined;
8172
+ return { type: head, ...(fromFile && { fromFile }) };
6668
8173
  }
6669
8174
  // Plain producer: Go same-package only; others unique-project-def with
6670
8175
  // same-file narrowing; Java bare calls reach the enclosing class first.
@@ -6698,8 +8203,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6698
8203
  return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
6699
8204
  }
6700
8205
  if (language === 'python' && !consumerAwaited && chosen.isAsync) return null;
6701
- let head = _structuralTypeHead(chosen.returnType, { unwrapAsync: consumerAwaited });
8206
+ let head = _structuralTypeHead(chosen.returnType, {
8207
+ unwrapAsync: consumerAwaited, index, language, originFile: chosen.file,
8208
+ });
6702
8209
  if (!head || /^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
8210
+ const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
8211
+ if (typeDefs.length > 0) {
8212
+ const origin = _resolveFlowTypeOrigin(index, chosen.file || filePath, head);
8213
+ if (!origin) return null;
8214
+ return { type: head, ...(origin.fromFile && { fromFile: origin.fromFile }) };
8215
+ }
6703
8216
  return { type: head };
6704
8217
  }
6705
8218
 
@@ -6712,17 +8225,41 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
6712
8225
  */
6713
8226
  function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
6714
8227
  if (!call.receiverCall || !call.receiverCallLine || !ctx.records) return null;
6715
- const prods = ctx.records.filter(r =>
6716
- r !== call && r.line === call.receiverCallLine && r.name === call.receiverCall &&
6717
- !r.isMacro && !r.inMacro &&
6718
- // Kind match: the consumer knows whether its producer was a method-
6719
- // shaped call — `.arg(arg("x"))` has both a chained method `arg` and
6720
- // a plain closure call `arg` on one line; only the right kind folds.
6721
- !!r.isMethod === !!call.receiverCallIsMethod);
8228
+ // A large builder-style function may contain hundreds of chained calls.
8229
+ // Scanning every record for every hop made one function O(calls²). Build
8230
+ // a context-local producer lookup once, then resolve each hop directly.
8231
+ if (!ctx.producerIndex) {
8232
+ ctx.producerIndex = new Map();
8233
+ for (const record of ctx.records) {
8234
+ if (record.isMacro || record.inMacro) continue;
8235
+ const key = `${record.line}\x00${record.name}\x00${record.isMethod ? 1 : 0}`;
8236
+ let group = ctx.producerIndex.get(key);
8237
+ if (!group) {
8238
+ group = [];
8239
+ ctx.producerIndex.set(key, group);
8240
+ }
8241
+ group.push(record);
8242
+ }
8243
+ }
8244
+ // Kind match: the consumer knows whether its producer was a method-shaped
8245
+ // call. `.arg(arg("x"))` has both a chained method `arg` and a plain
8246
+ // closure call `arg` on one line; only the right kind folds.
8247
+ const producerKey = `${call.receiverCallLine}\x00${call.receiverCall}\x00${call.receiverCallIsMethod ? 1 : 0}`;
8248
+ const prods = (ctx.producerIndex.get(producerKey) || []).filter(r => r !== call);
6722
8249
  if (prods.length === 0) return null;
6723
8250
  const results = prods.map(r =>
6724
8251
  _typeOfCallResultFold(index, fileEntry, filePath, r, ctx, call.receiverCallAwaited));
6725
- if (!results.every(Boolean)) return null;
8252
+ if (!results.every(Boolean)) {
8253
+ // A module owns its exported name. If module-qualified producer
8254
+ // resolution cannot type `z.number()`, the global same-name method
8255
+ // fallback is not allowed to borrow Mocker.number (or another
8256
+ // module/version) as its return type. Keep the consumer untyped and
8257
+ // visible instead of manufacturing exclusion-grade evidence.
8258
+ if (prods.some(r => r.receiverIsModule || _isStructuralImportReceiver(fileEntry, r))) {
8259
+ return { suppressFallback: true };
8260
+ }
8261
+ return null;
8262
+ }
6726
8263
  if (results.every(r => r.externalVia)) return { externalVia: results[0].externalVia };
6727
8264
  if (results.some(r => r.externalVia)) return null;
6728
8265
  if (new Set(results.map(r => r.type)).size !== 1) return null;
@@ -6730,6 +8267,16 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
6730
8267
  return { type: results[0].type, ...(fromFiles.size === 1 && results[0].fromFile && { fromFile: results[0].fromFile }) };
6731
8268
  }
6732
8269
 
8270
+ // A lower-case named import can be a namespace-like API object (`z.string()`)
8271
+ // even when the parser cannot label it receiverIsModule. Its terminal member
8272
+ // is still owned by that import; it must never borrow a same-named method's
8273
+ // return annotation from an unrelated class. Capitalized named imports remain
8274
+ // eligible for class/static-method resolution.
8275
+ function _isStructuralImportReceiver(fileEntry, record) {
8276
+ if (!record?.receiver || !/^[a-z_$]/.test(record.receiver)) return false;
8277
+ return (fileEntry?.importBindings || []).some(b => b.name === record.receiver);
8278
+ }
8279
+
6733
8280
  /**
6734
8281
  * Is this field symbol callable by its own name (obj.f(...) reaches the
6735
8282
  * field's function value)? Arrow-function class fields are callable by
@@ -6759,12 +8306,12 @@ function _buildTypedLocalTypeMap(index, def, calls) {
6759
8306
  for (const call of calls) {
6760
8307
  if (call.line < def.startLine || call.line > def.endLine) continue;
6761
8308
 
6762
- // Collect receiverType from method calls (inferred by parser from params/receivers)
6763
- if (call.isMethod && call.receiver && call.receiverType) {
6764
- localTypes.set(call.receiver, call.receiverType);
6765
- if (call.receiverTypeGuessed) guessedVars.add(call.receiver);
6766
- else guessedVars.delete(call.receiver);
6767
- }
8309
+ // Parser receiverType evidence belongs to that exact call record and
8310
+ // is consumed directly by findCallers/findCallees. Do not smear it
8311
+ // across the whole function: block shadowing (`if let Some(v) = ...`)
8312
+ // and reassignment can reuse the same variable name with a different
8313
+ // type. This map is reserved for explicit constructor-result inference
8314
+ // below, where the producer assignment is the evidence source.
6768
8315
 
6769
8316
  // Collect types from constructor calls: x := NewFoo() → x maps to Foo
6770
8317
  // Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()