ucn 5.3.3 → 5.3.5

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
@@ -12,8 +12,13 @@ const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('.
12
12
  const { isTestFile } = require('./discovery');
13
13
  const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath, CALLABLE_SYMBOL_KINDS } = require('./shared');
14
14
  const { _resolveJavaPackageImport } = require('./graph-build');
15
- const { scoreEdge, tierForResolution, TIER } = require('./confidence');
15
+ const { scoreEdge, tierForResolution, TIER, validateConfirmation } = require('./confidence');
16
+ const { summarizeProvenance, declarationIdentity, sameDeclaration } = require('./provenance');
17
+ const { confirmationFacts, occurrenceIdentity } = require('./provenance-facts');
18
+ const { BUILTIN_RECEIVER_TYPES, isProvenanceBuiltinReceiver } = require('./receiver-types');
16
19
  const { findGoModule, resolveRustImport } = require('./imports');
20
+ const { rustWrapperContract, validateRustWrapperContract } = require('./rust-result-flow');
21
+ const { pythonFixtureReceiver, pythonFixtureType } = require('./python-fixture-flow');
17
22
 
18
23
  const CONSTRUCTABLE_BINDING_KINDS = new Set([
19
24
  'class', 'struct', 'record', 'enum', 'function',
@@ -23,6 +28,45 @@ const RESERVED_RECEIVER_NAMES = new Set([
23
28
  ]);
24
29
  const CROSS_OPERATION_FLOW_CACHE_LIMIT = 4096;
25
30
 
31
+ function _confirmationFacts(index, file, call, targets, options = {}) {
32
+ return confirmationFacts(index, file, call, targets, {
33
+ ...options,
34
+ selectOverload: selectProvenanceOverload,
35
+ typeHead: name => _structuralTypeHead(name, { index, language: index.files.get(file)?.language }),
36
+ resolveType(name, context, line, qualified) {
37
+ const language = index.files.get(context)?.language;
38
+ const parts = name.split(name.includes('::') ? '::' : '.');
39
+ if (parts.length > 1) {
40
+ const simple = parts.pop();
41
+ const qualifier = parts.join(language === 'cpp' ? '::' : '.');
42
+ const named = (index.symbols.get(simple) || []).filter(d =>
43
+ IDENTITY_TYPE_KINDS.has(d.type) && d.namespace === qualifier &&
44
+ (d.file === context || index.importGraph.get(context)?.has(d.file)));
45
+ if (named.length === 1) return named[0];
46
+ }
47
+ if (language === 'java' || language === 'csharp') {
48
+ const definitions = (index.symbols.get(name) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
49
+ const selected = definitions.filter(d => _resolveReceiverTypeIdentity(
50
+ index, context, name, [{ ...d, className: name }], line, qualified) === 'target');
51
+ if (selected.length === 1) return selected[0];
52
+ }
53
+ if (language === 'java' && qualified) {
54
+ const nested = (index.symbols.get(name) || []).filter(d =>
55
+ IDENTITY_TYPE_KINDS.has(d.type) && d.enclosingType === qualified);
56
+ if (nested.length === 1) return nested[0];
57
+ }
58
+ const origin = _resolveFlowTypeOrigin(index, context, name, qualified);
59
+ if (!origin?.fromFile) return null;
60
+ const definitions = (index.symbols.get(name) || []).filter(d =>
61
+ (IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf)) &&
62
+ d.file === origin.fromFile &&
63
+ (!d.lexicalScopeStartLine || (line != null &&
64
+ line >= d.lexicalScopeStartLine && line <= d.lexicalScopeEndLine)));
65
+ return definitions.length === 1 ? definitions[0] : null;
66
+ },
67
+ });
68
+ }
69
+
26
70
  // `base` is a contextual receiver keyword only in C#. TypeScript and the
27
71
  // other supported languages may bind an ordinary local named base; treating
28
72
  // it as reserved globally suppresses otherwise exact assignment flow.
@@ -290,8 +334,9 @@ function findCallers(index, name, options = {}) {
290
334
  // lookup); the rest stay as shadow-style records. Display caps are handled
291
335
  // by formatters — this only bounds file reads.
292
336
  const unverifiedEnrichLimit = options.unverifiedEnrichLimit ?? 10;
293
- const recordExcluded = (filePath, line, reason) => {
294
- if (accountRaw) accountRaw.excludedEntries.push({ file: filePath, line, reason });
337
+ const recordExcluded = (filePath, line, reason, provenance) => {
338
+ if (accountRaw) accountRaw.excludedEntries.push({ file: filePath, line, reason,
339
+ ...(provenance && { provenance }) });
295
340
  };
296
341
 
297
342
  const definitions = index.symbols.get(name) || [];
@@ -556,7 +601,7 @@ function findCallers(index, name, options = {}) {
556
601
  // unverified-tier entry (tiered caller contract: shown in its own
557
602
  // section, never silently hidden). Does NOT count toward pendingCount —
558
603
  // totals describe the confirmed answer.
559
- const routeUnverified = (filePath, fileEntry, call, reason, calledAs, meta) => {
604
+ const routeUnverified = (filePath, fileEntry, call, reason, calledAs, meta, facts) => {
560
605
  if (!collectAccount) return; // non-account paths (trace/blast/verify) keep the plain drop
561
606
  const compilerSelectsOverload = cppOverloadDispatchTarget &&
562
607
  (reason === 'overload-ambiguous' || reason === 'ambiguous-binding' ||
@@ -582,11 +627,28 @@ function findCallers(index, name, options = {}) {
582
627
  _tier: TIER.UNVERIFIED, _reason: reason, _meta: meta,
583
628
  // Dispatch-tiered routes carry their own resolution so JSON output
584
629
  // distinguishes "possible virtual dispatch" from a bare uncertain.
585
- _evidence: reason === 'possible-dispatch' ? { possibleDispatch: true }
630
+ _evidence: { ...(reason === 'single-owner' ? { hasSingleOwnerEvidence: true }
631
+ : reason === 'possible-dispatch' ? { possibleDispatch: true }
586
632
  : reason === 'method-ambiguous' ? { methodAmbiguous: true }
587
- : { isUncertain: true },
633
+ : { isUncertain: true }), ...(facts && { facts }) },
588
634
  });
589
635
  };
636
+ const excludeReceiver = (filePath, fileEntry, call, calledAs, receiverOptions = {}) => {
637
+ const targets = options.targetDefinitions || definitions;
638
+ const facts = _confirmationFacts(index, filePath, call, targets, receiverOptions);
639
+ if ((BUILTIN_RECEIVER_TYPES.has(facts.receiverType) ||
640
+ (fileEntry.language === 'rust' && ['Option', 'Result', 'Vec'].includes(facts.receiverType))) &&
641
+ (facts.receiverTypeSource === 'literal' || !facts.receiverTypeDeclaration)) {
642
+ facts.builtinReceiver = { type: facts.receiverType, language: fileEntry.language };
643
+ }
644
+ const provenance = scoreEdge({ hasReceiverType: true, facts }).provenance;
645
+ const checked = validateConfirmation(provenance, facts.targets);
646
+ if (checked.verdict === 'establishes-other' || checked.verdict === 'unsupported') {
647
+ recordExcluded(filePath, call.line, 'receiver-type-mismatch', provenance);
648
+ } else {
649
+ routeUnverified(filePath, fileEntry, call, 'provenance-incomplete', calledAs, undefined, facts);
650
+ }
651
+ };
590
652
  const maxResults = options.maxResults;
591
653
  // BUG-H1: when consumers (like `about`) need an accurate truncation header
592
654
  // ("showing N of <total>"), they pass needsTotal:true so Phase 1 runs to
@@ -670,6 +732,21 @@ function findCallers(index, name, options = {}) {
670
732
  calledAs = call.name;
671
733
  }
672
734
 
735
+ const standardWrapper = fileEntry.language === 'rust'
736
+ ? _rustStandardWrapperMethod(index, filePath, call, calls) : null;
737
+ if (standardWrapper) {
738
+ const facts = { language: 'rust', site: occurrenceIdentity(fileEntry.relativePath, call),
739
+ targets: (options.targetDefinitions || definitions).map(declarationIdentity), standardWrapper };
740
+ const checked = validateConfirmation({ facts }, facts.targets);
741
+ if (checked.verdict === 'establishes-other') {
742
+ recordExcluded(filePath, call.line, 'standard-wrapper-method', {
743
+ rule: 'rust-standard-wrapper', rules: ['rust-standard-wrapper'], facts,
744
+ validation: checked.verdict, diagnostic: 'standard-wrapper-method',
745
+ });
746
+ continue;
747
+ }
748
+ }
749
+
673
750
  // A direct static call cannot cross an unrelated runtime
674
751
  // language boundary. A Python `service.get` is not a possible
675
752
  // target of JavaScript `map.get()`, nor can a Java method be
@@ -692,6 +769,8 @@ function findCallers(index, name, options = {}) {
692
769
  call = {
693
770
  ...call,
694
771
  receiverType: indexedType.type,
772
+ receiverTypeSource: 'flow',
773
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
695
774
  ...(indexedType.fromFile && {
696
775
  receiverTypeFlowFile: indexedType.fromFile,
697
776
  }),
@@ -925,7 +1004,7 @@ function findCallers(index, name, options = {}) {
925
1004
  // persisted with this file's calls.
926
1005
  if (call.isMethod && call.receiver &&
927
1006
  !_isReservedReceiver(fileEntry.language, call.receiver) &&
928
- (!call.receiverType || call.receiverTypeGuessed) &&
1007
+ (!call.receiverType || call.receiverTypeGuessed || call.receiverTypeSource === 'guess') &&
929
1008
  !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
930
1009
  !call.receiverIsChainRoot &&
931
1010
  (langTraits(fileEntry.language)?.typeSystem === 'structural' ||
@@ -954,7 +1033,7 @@ function findCallers(index, name, options = {}) {
954
1033
  receiverExternalConcreteFlow: true,
955
1034
  }),
956
1035
  };
957
- if (call.receiverTypeGuessed) {
1036
+ if (call.receiverTypeGuessed || call.receiverTypeSource === 'guess') {
958
1037
  call = { ...call, receiverType: undefined, receiverTypeGuessed: undefined };
959
1038
  }
960
1039
  } else if (flowEntry) {
@@ -965,12 +1044,30 @@ function findCallers(index, name, options = {}) {
965
1044
  // annotation says *DefaultCodecRegistry; the guess
966
1045
  // excluded all three true RegisterCodec callers).
967
1046
  call = { ...call, receiverType: flowEntry.type,
1047
+ receiverTypeSource: 'flow',
1048
+ receiverTypeEvidence: { source: 'flow', ...flowEntry },
968
1049
  receiverTypeGuessed: undefined,
969
1050
  receiverFlowInvalidated: false,
970
1051
  ...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
971
1052
  }
972
1053
  }
973
1054
 
1055
+ if (fileEntry.language === 'python' && call.isMethod && !call.receiverType) {
1056
+ const fixture = _pythonFixtureReceiverType(index, filePath, call);
1057
+ if (fixture) call = { ...call, receiverType: fixture.type,
1058
+ receiverTypeSource: 'fixture', receiverTypeEvidence: { source: 'fixture', ...fixture },
1059
+ ...(fixture.fromFile && { receiverTypeFlowFile: fixture.fromFile }) };
1060
+ const external = !fixture && _pythonExternalFieldFlow(index, filePath, call);
1061
+ if (external) {
1062
+ call = { ...call, receiverTypeSource: 'flow',
1063
+ receiverTypeEvidence: { source: 'flow', externalFactory: external.proof } };
1064
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs,
1065
+ { dispatchVia: external.via, externalContract: true },
1066
+ _confirmationFacts(index, filePath, call, options.targetDefinitions || definitions));
1067
+ continue;
1068
+ }
1069
+ }
1070
+
974
1071
  // Python indexed receivers (fix #324): `layout["body"].update()`
975
1072
  // dispatch through the container's compiler-visible
976
1073
  // `__getitem__` return contract. The parser retains only a
@@ -992,6 +1089,8 @@ function findCallers(index, name, options = {}) {
992
1089
  call = {
993
1090
  ...call,
994
1091
  receiverType: indexedType.type,
1092
+ receiverTypeSource: 'flow',
1093
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
995
1094
  ...(indexedType.fromFile && {
996
1095
  receiverTypeFlowFile: indexedType.fromFile,
997
1096
  }),
@@ -1028,7 +1127,9 @@ function findCallers(index, name, options = {}) {
1028
1127
  const items = _pythonDeclaredIterablePathItems(
1029
1128
  index, rootType, call.receiverIterationFields);
1030
1129
  const item = items?.[call.receiverIterationIndex || 0];
1031
- if (item) call = { ...call, receiverType: item };
1130
+ if (item) call = { ...call, receiverType: item,
1131
+ receiverTypeSource: 'flow',
1132
+ receiverTypeEvidence: { source: 'flow', type: item, iteration: call.receiver } };
1032
1133
  }
1033
1134
  }
1034
1135
 
@@ -1038,6 +1139,8 @@ function findCallers(index, name, options = {}) {
1038
1139
  index, fileEntry, call.receiver);
1039
1140
  if (importedType) {
1040
1141
  call = { ...call, receiverType: importedType.type,
1142
+ receiverTypeSource: 'flow',
1143
+ receiverTypeEvidence: { source: 'flow', ...importedType },
1041
1144
  receiverTypeFlowFile: importedType.fromFile };
1042
1145
  }
1043
1146
  }
@@ -1082,6 +1185,9 @@ function findCallers(index, name, options = {}) {
1082
1185
  call = {
1083
1186
  ...call,
1084
1187
  receiverType: returnInfo.type.replace(/\[\]$/, '').split('.').pop(),
1188
+ receiverTypeSource: 'flow',
1189
+ receiverTypeEvidence: { source: 'flow', ...returnInfo,
1190
+ producerPath: call.receiverCallTypePath },
1085
1191
  ...(returnInfo.fromFile && {
1086
1192
  receiverTypeFlowFile: returnInfo.fromFile,
1087
1193
  }),
@@ -1113,7 +1219,7 @@ function findCallers(index, name, options = {}) {
1113
1219
  // exact indexed variant and positional payload before method
1114
1220
  // dispatch; external qualified payloads remain external
1115
1221
  // provenance, never borrowed project identity.
1116
- if (collectAccount && fileEntry.language === 'rust' &&
1222
+ if ((collectAccount || call.receiverPatternSourceCallStart != null) && fileEntry.language === 'rust' &&
1117
1223
  call.isMethod && !call.receiverType &&
1118
1224
  call.receiverPatternVariant) {
1119
1225
  const patternType = _rustPatternReceiverType(
@@ -1122,6 +1228,8 @@ function findCallers(index, name, options = {}) {
1122
1228
  call = {
1123
1229
  ...call,
1124
1230
  receiverType: patternType.type,
1231
+ receiverTypeSource: 'flow',
1232
+ receiverTypeEvidence: { source: 'flow', ...patternType },
1125
1233
  ...(patternType.fromFile && {
1126
1234
  receiverTypeFlowFile: patternType.fromFile,
1127
1235
  }),
@@ -1246,10 +1354,14 @@ function findCallers(index, name, options = {}) {
1246
1354
  const builtinChained = _pythonBuiltinChainedReceiverType(
1247
1355
  index, fileEntry, call, foldCtx);
1248
1356
  if (builtinChained) {
1249
- call = { ...call, receiverType: builtinChained,
1357
+ call = { ...call, receiverType: builtinChained.type,
1358
+ receiverTypeSource: 'flow',
1359
+ receiverTypeEvidence: { source: 'flow', ...builtinChained },
1250
1360
  receiverTypePlatform: true };
1251
1361
  } else if (folded && folded.type) {
1252
1362
  call = { ...call, receiverType: folded.type,
1363
+ receiverTypeSource: 'flow',
1364
+ receiverTypeEvidence: { source: 'flow', ...folded },
1253
1365
  ...(folded.fromFile && { receiverTypeFlowFile: folded.fromFile }) };
1254
1366
  } else if (folded && folded.externalVia) {
1255
1367
  call = {
@@ -1262,6 +1374,8 @@ function findCallers(index, name, options = {}) {
1262
1374
  } else if (!folded?.suppressFallback) {
1263
1375
  const chainedType = _chainedReceiverType(index, call, fileEntry.language);
1264
1376
  if (chainedType) call = { ...call, receiverType: chainedType.type,
1377
+ receiverTypeSource: 'flow',
1378
+ receiverTypeEvidence: { source: 'flow', ...chainedType },
1265
1379
  ...(chainedType.fromFile && { receiverTypeFlowFile: chainedType.fromFile }) };
1266
1380
  }
1267
1381
  } else if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
@@ -1303,6 +1417,8 @@ function findCallers(index, name, options = {}) {
1303
1417
  };
1304
1418
  } else if (flowEntry) {
1305
1419
  call = { ...call, receiverType: flowEntry.type,
1420
+ receiverTypeSource: 'flow',
1421
+ receiverTypeEvidence: { source: 'flow', ...flowEntry },
1306
1422
  ...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
1307
1423
  }
1308
1424
  }
@@ -1313,7 +1429,7 @@ function findCallers(index, name, options = {}) {
1313
1429
  // analysis resolves the source owner and its declared
1314
1430
  // `Iterator<Item = T>` contract. Item-preserving std adapters
1315
1431
  // such as rev/filter/take recurse to that same declaration.
1316
- if (collectAccount && fileEntry.language === 'rust' &&
1432
+ if (fileEntry.language === 'rust' &&
1317
1433
  call.isMethod && call.receiver && !call.receiverType &&
1318
1434
  (call.receiverIterationCall || call.receiverIterationVariable)) {
1319
1435
  let foldCtx = foldCtxCache.get(filePath);
@@ -1329,37 +1445,14 @@ function findCallers(index, name, options = {}) {
1329
1445
  } };
1330
1446
  foldCtxCache.set(filePath, foldCtx);
1331
1447
  }
1332
- let items = [];
1333
- if (call.receiverIterationVariable) {
1334
- const flow = _lookupReturnTypeFlow(foldCtx.getFlowMap(), {
1335
- ...call,
1336
- receiver: call.receiverIterationVariable,
1337
- });
1338
- if (flow?.iteratorItemType) {
1339
- items = [{
1340
- type: flow.iteratorItemType,
1341
- fromFile: flow.iteratorItemFromFile,
1342
- }];
1343
- }
1344
- } else {
1345
- const sources = _chainedProducerRecords(foldCtx, {
1346
- receiverCall: call.receiverIterationCall,
1347
- receiverCallIsMethod: call.receiverIterationCallIsMethod,
1348
- receiverCallLine: call.receiverIterationCallLine,
1349
- receiverCallStart: call.receiverIterationCallStart,
1350
- receiverCallEnd: call.receiverIterationCallEnd,
1351
- });
1352
- items = sources.map(source => _rustIteratorOutputItemType(
1353
- index, fileEntry, filePath, source, foldCtx));
1354
- }
1355
- if (items.length > 0 && items.every(Boolean) &&
1356
- new Set(items.map(item => item.type)).size === 1 &&
1357
- new Set(items.map(item => item.fromFile)).size === 1 &&
1358
- items[0].fromFile) {
1448
+ const item = _rustIterationReceiverType(index, fileEntry, filePath, call, foldCtx);
1449
+ if (item) {
1359
1450
  call = {
1360
1451
  ...call,
1361
- receiverType: items[0].type,
1362
- receiverTypeFlowFile: items[0].fromFile,
1452
+ receiverType: item.type,
1453
+ receiverTypeSource: 'flow',
1454
+ receiverTypeEvidence: { source: 'flow', ...item },
1455
+ receiverTypeFlowFile: item.fromFile,
1363
1456
  };
1364
1457
  }
1365
1458
  }
@@ -1393,6 +1486,8 @@ function findCallers(index, name, options = {}) {
1393
1486
  call = {
1394
1487
  ...call,
1395
1488
  receiverType: closureType.type,
1489
+ receiverTypeSource: 'flow',
1490
+ receiverTypeEvidence: { source: 'flow', ...closureType },
1396
1491
  receiverTypeFlowFile: closureType.fromFile,
1397
1492
  };
1398
1493
  }
@@ -1590,7 +1685,7 @@ function findCallers(index, name, options = {}) {
1590
1685
  dispatchCandidates: countDispatchCandidates(call.receiverType),
1591
1686
  });
1592
1687
  } else {
1593
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
1688
+ excludeReceiver(filePath, fileEntry, call, calledAs);
1594
1689
  }
1595
1690
  }
1596
1691
  continue;
@@ -1619,9 +1714,9 @@ function findCallers(index, name, options = {}) {
1619
1714
  const cbTypedMatch = call.receiverType && cbTypes.has(call.receiverType);
1620
1715
  const cbAllTypeTargets = cbTargetDefs.length > 0 &&
1621
1716
  cbTargetDefs.every(d => IDENTITY_TYPE_KINDS.has(d.type));
1622
- if (!cbTypeQualified && !cbTypedMatch &&
1623
- (methodOwnerKeys().size > 1 || cbAllTypeTargets)) {
1624
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs,
1717
+ if (!cbTypeQualified && !cbTypedMatch) {
1718
+ routeUnverified(filePath, fileEntry, call,
1719
+ methodOwnerKeys().size === 1 && !cbAllTypeTargets ? 'single-owner' : 'method-ambiguous', calledAs,
1625
1720
  { dispatchCandidates: methodOwnerKeys().size });
1626
1721
  continue;
1627
1722
  }
@@ -2005,6 +2100,7 @@ function findCallers(index, name, options = {}) {
2005
2100
  // resolution, but its evidence grade is receiver-hint, not
2006
2101
  // same-class (the receiver is the field's class).
2007
2102
  let resolvedByTypedAttribute = false;
2103
+ let resolvedReceiverFacts = {};
2008
2104
  // Receiver/path type known to mismatch the target: such an edge can
2009
2105
  // never tier as confirmed even when legacy includeUncertain keeps it
2010
2106
  // visible (scoreEdge checks hasReceiverType before isUncertain, so
@@ -2053,6 +2149,9 @@ function findCallers(index, name, options = {}) {
2053
2149
  index, fileEntry, call.receiver);
2054
2150
  if (staticFieldType) {
2055
2151
  call = { ...call, receiverType: staticFieldType,
2152
+ receiverTypeSource: 'field',
2153
+ receiverTypeEvidence: { source: 'field', field: call.receiver,
2154
+ type: staticFieldType, imports: fileEntry.importBindings },
2056
2155
  receiverIsTypeQualified: false };
2057
2156
  }
2058
2157
  }
@@ -2160,6 +2259,12 @@ function findCallers(index, name, options = {}) {
2160
2259
  if (compatibleTypes.has(targetClass)) {
2161
2260
  resolvedBySameClass = true;
2162
2261
  resolvedByTypedAttribute = true;
2262
+ resolvedReceiverFacts = {
2263
+ receiverType: targetClass, receiverTypeSource: 'field',
2264
+ receiverOrigin: { source: 'field', rootType: callerSymbol.className,
2265
+ field: call.selfAttribute, scope: declarationIdentity(callerSymbol),
2266
+ ...attrTypes?.origins?.get(call.selfAttribute) },
2267
+ };
2163
2268
  } else if (_isAncestorOfTargetClass(index, targetClass, tDefs)) {
2164
2269
  // A field declared as a strict ancestor can
2165
2270
  // dynamically hold the pinned override.
@@ -2172,7 +2277,11 @@ function findCallers(index, name, options = {}) {
2172
2277
  // type is exact negative evidence. Earlier
2173
2278
  // code discarded this field contract and
2174
2279
  // emitted method-no-evidence.
2175
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
2280
+ excludeReceiver(filePath, fileEntry, call, calledAs, {
2281
+ receiverType: targetClass, receiverTypeSource: 'field',
2282
+ receiverOrigin: { source: 'field', rootType: callerSymbol?.className,
2283
+ field: call.selfAttribute, ...attrTypes?.origins?.get(call.selfAttribute) },
2284
+ });
2176
2285
  continue;
2177
2286
  } else if (options.collectAccount || !options.includeMethods) {
2178
2287
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
@@ -2351,7 +2460,9 @@ function findCallers(index, name, options = {}) {
2351
2460
  const builtinFieldType = _pythonBuiltinFieldPathType(
2352
2461
  index, fileEntry, call.receiverRoot, call.receiverFields);
2353
2462
  if (builtinFieldType) {
2354
- call = { ...call, receiverType: builtinFieldType,
2463
+ call = { ...call, receiverType: builtinFieldType.type,
2464
+ receiverTypeSource: 'field',
2465
+ receiverTypeEvidence: { source: 'field', ...builtinFieldType },
2355
2466
  receiverTypePlatform: true };
2356
2467
  }
2357
2468
  }
@@ -3036,6 +3147,13 @@ function findCallers(index, name, options = {}) {
3036
3147
  continue;
3037
3148
  }
3038
3149
  const targetHasClass = targetDefs.some(d => d.className);
3150
+ if (call.isMethod && !targetHasClass && fileEntry.language === 'c') {
3151
+ // C member calls invoke function-pointer fields. The
3152
+ // pointer can hold the pinned free function; member
3153
+ // syntax alone is neither binding nor exclusion proof.
3154
+ routeUnverified(filePath, fileEntry, call, 'callable-field', calledAs);
3155
+ continue;
3156
+ }
3039
3157
  if (call.isMethod && !targetHasClass) {
3040
3158
  // Method call but target is a standalone function — skip
3041
3159
  recordExcluded(filePath, call.line, 'method-kind-mismatch');
@@ -3227,7 +3345,7 @@ function findCallers(index, name, options = {}) {
3227
3345
  isUncertain = true;
3228
3346
  typeMismatch = true;
3229
3347
  if (collectAccount) {
3230
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3348
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3231
3349
  continue;
3232
3350
  }
3233
3351
  if (!options.includeUncertain) {
@@ -3303,7 +3421,7 @@ function findCallers(index, name, options = {}) {
3303
3421
  isUncertain = true;
3304
3422
  typeMismatch = true;
3305
3423
  if (collectAccount) {
3306
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3424
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3307
3425
  continue;
3308
3426
  }
3309
3427
  if (!options.includeUncertain) {
@@ -3312,6 +3430,14 @@ function findCallers(index, name, options = {}) {
3312
3430
  }
3313
3431
  }
3314
3432
 
3433
+ // C# dynamic is an unresolved runtime receiver, never a
3434
+ // concrete foreign type that can disprove a project edge.
3435
+ if (fileEntry.language === 'csharp' && call.receiverType === 'dynamic') {
3436
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs,
3437
+ { dispatchVia: 'dynamic' });
3438
+ continue;
3439
+ }
3440
+
3315
3441
  // Receiver-class disambiguation:
3316
3442
  // When the target definition has a class/receiver type, filter callers
3317
3443
  // whose receiverType is known to be a different type.
@@ -3571,7 +3697,8 @@ function findCallers(index, name, options = {}) {
3571
3697
  // Not evidence against — visible possible-dispatch.
3572
3698
  // Go struct embedding binds statically and stays
3573
3699
  // excluded.
3574
- if (_dispatchCapableSupertype(index, fileEntry.language, knownType, targetDefs, definitions)) {
3700
+ if (_dispatchCapableSupertype(index, fileEntry.language, knownType, targetDefs, definitions) ||
3701
+ (!knownTypeHasProjectIdentity && externalContractTarget()?.via === knownType)) {
3575
3702
  const externalContract = externalContractTarget();
3576
3703
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
3577
3704
  dispatchVia: knownType,
@@ -3582,7 +3709,11 @@ function findCallers(index, name, options = {}) {
3582
3709
  });
3583
3710
  continue;
3584
3711
  }
3585
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3712
+ excludeReceiver(filePath, fileEntry, call, calledAs, fieldHopType && !call.receiverType ? {
3713
+ receiverType: fieldHopType, receiverTypeSource: 'field',
3714
+ receiverOrigin: { source: 'field', root: call.receiverRoot,
3715
+ rootType: call.receiverRootType, field: call.receiverField },
3716
+ } : {});
3586
3717
  continue;
3587
3718
  }
3588
3719
  if (!options.includeUncertain) {
@@ -3695,7 +3826,7 @@ function findCallers(index, name, options = {}) {
3695
3826
  isUncertain = true;
3696
3827
  typeMismatch = true;
3697
3828
  if (collectAccount) {
3698
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3829
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3699
3830
  continue;
3700
3831
  }
3701
3832
  if (!options.includeUncertain) {
@@ -3736,6 +3867,10 @@ function findCallers(index, name, options = {}) {
3736
3867
  }
3737
3868
  inferredMatch = true;
3738
3869
  nominalInferredMatch = true;
3870
+ resolvedReceiverFacts = {
3871
+ receiverType: typeReceiver, receiverTypeSource: 'type-qualified',
3872
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
3873
+ };
3739
3874
  }
3740
3875
  // Still no type — fall back to receiver name matching when
3741
3876
  // multiple defs exist. A field-declared interface/trait type
@@ -4154,6 +4289,13 @@ function findCallers(index, name, options = {}) {
4154
4289
  continue;
4155
4290
  }
4156
4291
  }
4292
+ if (typeQualifiedReceiver) {
4293
+ resolvedReceiverFacts = {
4294
+ receiverType: receiverName, receiverTypeSource: 'type-qualified',
4295
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
4296
+ ...(aliasResolvedFile && { originFile: aliasResolvedFile }),
4297
+ };
4298
+ }
4157
4299
  if (!typeQualifiedReceiver) {
4158
4300
  // External-producer receiver (fix #220): the variable
4159
4301
  // was assigned from a call into an external package
@@ -4344,6 +4486,15 @@ function findCallers(index, name, options = {}) {
4344
4486
  }
4345
4487
  }
4346
4488
  }
4489
+ // Module ownership is evidence for an exported item,
4490
+ // not for a method nested inside that module's type.
4491
+ // A same-named free function and method can share the
4492
+ // target file (itertools::peek_nth vs PeekNth::peek_nth).
4493
+ if (call.moduleOwnedPath && fileEntry.language === 'rust' &&
4494
+ targetDefs2.every(d => d.className || d.receiver)) {
4495
+ routeUnverified(filePath, fileEntry, call, 'provenance-incomplete', calledAs);
4496
+ continue;
4497
+ }
4347
4498
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
4348
4499
  // Module-owned qualified calls (fix #260b) are resolved
4349
4500
  // by OWNERSHIP — the dispatch-ambiguity routing below is
@@ -4373,8 +4524,11 @@ function findCallers(index, name, options = {}) {
4373
4524
  });
4374
4525
  continue;
4375
4526
  }
4376
- if (!call.moduleOwnedPath && !knownDispatchType && methodOwnerKeys().size > 1) {
4377
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
4527
+ if (!call.moduleOwnedPath && !knownDispatchType) {
4528
+ const contract = externalContractTarget();
4529
+ routeUnverified(filePath, fileEntry, call,
4530
+ contract ? 'possible-dispatch' : methodOwnerKeys().size === 1 ? 'single-owner' : 'method-ambiguous', calledAs, {
4531
+ ...(contract && { dispatchVia: contract.via, externalContract: true }),
4378
4532
  dispatchCandidates: methodOwnerKeys().size,
4379
4533
  });
4380
4534
  continue;
@@ -4566,8 +4720,8 @@ function findCallers(index, name, options = {}) {
4566
4720
  // in a file that imports _decoders.py is bytes.decode, not
4567
4721
  // ContentDecoder.decode. An untyped-receiver method call
4568
4722
  // confirms only via binding, same-class, a validated receiver
4569
- // type, a type-qualified receiver (Class.method static style),
4570
- // or a single project-wide owner. Multi-owner name matches
4723
+ // type or a type-qualified receiver (Class.method static style).
4724
+ // Single-owner and multi-owner name matches
4571
4725
  // route VISIBLE method-ambiguous — never dropped. Same for a
4572
4726
  // bare call against pure method targets (a bare name cannot
4573
4727
  // denote a method in JS/TS/Python — only a rebound alias can,
@@ -4583,7 +4737,11 @@ function findCallers(index, name, options = {}) {
4583
4737
  if (call.isMethod && !call.receiverIsModule &&
4584
4738
  !recvSubmoduleRel && !call.moduleOwnedPath) {
4585
4739
  const tTypes = dispatchTargetTypes(targetDefs2);
4586
- const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
4740
+ let typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
4741
+ if (typeQualifiedReceiver) resolvedReceiverFacts = {
4742
+ receiverType: call.receiver, receiverTypeSource: 'type-qualified',
4743
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
4744
+ };
4587
4745
  const knownDispatchType = call.receiverType ||
4588
4746
  fieldHopType || fieldDispatchType;
4589
4747
  // A compiler/parser-known receiver type that is not a
@@ -4758,6 +4916,8 @@ function findCallers(index, name, options = {}) {
4758
4916
  });
4759
4917
  continue;
4760
4918
  }
4919
+ typeQualifiedReceiver = true;
4920
+ call = { ...call, moduleOwnedPath: true };
4761
4921
  }
4762
4922
  // External-contract single owner (fix #210): same
4763
4923
  // physics as the nominal gate above — an override
@@ -4787,7 +4947,7 @@ function findCallers(index, name, options = {}) {
4787
4947
  });
4788
4948
  continue;
4789
4949
  }
4790
- if (!typeQualifiedReceiver && methodOwnerKeys().size > 1) {
4950
+ if (!typeQualifiedReceiver) {
4791
4951
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
4792
4952
  if (knownDispatchType) {
4793
4953
  // Known-but-unvalidated type (supertype of the
@@ -4801,7 +4961,7 @@ function findCallers(index, name, options = {}) {
4801
4961
  dispatchCandidates: countDispatchCandidates(knownDispatchType),
4802
4962
  });
4803
4963
  } else {
4804
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
4964
+ routeUnverified(filePath, fileEntry, call, methodOwnerKeys().size === 1 ? 'single-owner' : 'method-ambiguous', calledAs, {
4805
4965
  dispatchCandidates: methodOwnerKeys().size,
4806
4966
  });
4807
4967
  }
@@ -4876,6 +5036,17 @@ function findCallers(index, name, options = {}) {
4876
5036
  }
4877
5037
  }
4878
5038
 
5039
+ if (!collectAccount && call.isMethod && !call.isConstructor &&
5040
+ !resolvedBySameClass && !receiverTypeValidated && !nominalInferredMatch &&
5041
+ !resolvedByExtensionMethod && !call.moduleOwnedPath &&
5042
+ !call.receiverIsModule && !recvSubmoduleRel) {
5043
+ const qualified = _calleeTypeQualifiedReceiver(index,
5044
+ { ...callerSymbol, file: filePath }, fileEntry, call, fileEntry.language);
5045
+ const exact = qualified?.match && targetDefs.some(target =>
5046
+ target.file === qualified.match.file && target.startLine === qualified.match.startLine);
5047
+ if (!exact) continue;
5048
+ }
5049
+
4879
5050
  if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
4880
5051
  pendingByFile.get(filePath).push({
4881
5052
  call, fileEntry, callerSymbol,
@@ -4891,6 +5062,19 @@ function findCallers(index, name, options = {}) {
4891
5062
  receiverType: call.receiverType,
4892
5063
  calledAs,
4893
5064
  _evidence: {
5065
+ facts: _confirmationFacts(index, filePath, call, targetDefs, {
5066
+ bindingId,
5067
+ ...resolvedReceiverFacts,
5068
+ sameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
5069
+ ...(fieldHopType && !call.receiverType && {
5070
+ receiverType: fieldHopType, receiverTypeSource: 'field',
5071
+ receiverOrigin: { source: 'field', root: call.receiverRoot,
5072
+ rootType: call.receiverRootType, field: call.receiverField },
5073
+ }),
5074
+ }),
5075
+ typeQualifiedReceiver: resolvedReceiverFacts.receiverTypeSource === 'type-qualified',
5076
+ moduleOwnedPath: !!call.moduleOwnedPath,
5077
+ extensionMethod: !!resolvedByExtensionMethod,
4894
5078
  hasBindingId: !!bindingId,
4895
5079
  resolvedBySameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
4896
5080
  hasSamePackageEvidence,
@@ -4912,8 +5096,8 @@ function findCallers(index, name, options = {}) {
4912
5096
  // The dispatch gates above have already rejected
4913
5097
  // external contracts, universal methods, wrong arity,
4914
5098
  // unresolved producer flow, and multi-owner names.
4915
- // What remains with exactly one project owner is
4916
- // positive project-scope identity evidence.
5099
+ // Owner count describes unresolved candidates. It
5100
+ // never establishes the identity of a value receiver (#355).
4917
5101
  hasSingleOwnerEvidence: !!(collectAccount && call.isMethod &&
4918
5102
  !call.inMacroDefinition && !call.isMacro &&
4919
5103
  !call.receiverType && !fieldHopType &&
@@ -4967,14 +5151,43 @@ function findCallers(index, name, options = {}) {
4967
5151
  for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference,
4968
5152
  isTypeReference, receiver, receiverType, calledAs, _evidence, _tier,
4969
5153
  _reason, _meta } of pending) {
4970
- const scored = scoreEdge(_evidence || {});
5154
+ const evidence = {
5155
+ ...(_evidence || {}),
5156
+ ...(_reason && { reason: _reason }),
5157
+ facts: _evidence?.facts || _confirmationFacts(index, filePath,
5158
+ call, options.targetDefinitions || definitions, {
5159
+ sameClass: _evidence?.resolvedBySameClass,
5160
+ }),
5161
+ };
5162
+ let scored = scoreEdge(evidence);
5163
+ let routedTier = _tier;
5164
+ let routedReason = _reason;
5165
+ const migrated = isMethod && !call.isConstructor && !evidence.resolvedBySameClass &&
5166
+ !evidence.typeQualifiedReceiver && !evidence.moduleOwnedPath && !evidence.extensionMethod &&
5167
+ (evidence.hasReceiverType || evidence.resolvedByReceiverHint || evidence.hasSingleOwnerEvidence);
5168
+ if (!_tier && migrated && collectAccount) {
5169
+ const checked = validateConfirmation(scored.provenance, evidence.facts.targets);
5170
+ if (checked.verdict === 'establishes-other') {
5171
+ recordExcluded(filePath, call.line, 'receiver-target-different', scored.provenance);
5172
+ continue;
5173
+ }
5174
+ if (checked.verdict !== 'establishes-target' && checked.verdict !== 'unsupported') {
5175
+ if (!collectAccount) continue;
5176
+ routedTier = TIER.UNVERIFIED;
5177
+ routedReason = evidence.hasSingleOwnerEvidence && !evidence.hasReceiverType
5178
+ ? 'single-owner' : 'provenance-incomplete';
5179
+ const ranking = scoreEdge({ isUncertain: true,
5180
+ hasSingleOwnerEvidence: routedReason === 'single-owner' });
5181
+ scored = { ...ranking, provenance: scored.provenance };
5182
+ }
5183
+ }
4971
5184
  // Family B contract field (fix #221): a bind/call/apply site reaches
4972
5185
  // the target through Function.prototype indirection, not direct call
4973
5186
  // syntax — label the edge calledAs:'bound'. Rename aliases keep their
4974
5187
  // surface name (they describe the same slot and are rarer). Label
4975
5188
  // only, computed at edge construction: routing logic never sees it.
4976
5189
  const edgeCalledAs = calledAs || (call.boundCall ? 'bound' : undefined);
4977
- if (_tier) {
5190
+ if (routedTier) {
4978
5191
  // Routed unverified entry — never competes with the main
4979
5192
  // answer for maxResults/enrichLimit slots.
4980
5193
  const base = {
@@ -4986,8 +5199,9 @@ function findCallers(index, name, options = {}) {
4986
5199
  evidenceScore: scored.evidenceScore,
4987
5200
  scoreKind: scored.scoreKind,
4988
5201
  resolution: scored.resolution,
4989
- tier: _tier,
4990
- reason: _reason,
5202
+ ...(collectAccount && { provenance: scored.provenance }),
5203
+ tier: routedTier,
5204
+ reason: routedReason,
4991
5205
  ...(_meta || {}),
4992
5206
  isMethod: call.isMethod || false,
4993
5207
  ...(isFunctionReference && { isFunctionReference: true }),
@@ -5036,6 +5250,7 @@ function findCallers(index, name, options = {}) {
5036
5250
  evidenceScore: scored.evidenceScore,
5037
5251
  scoreKind: scored.scoreKind,
5038
5252
  resolution: scored.resolution,
5253
+ ...(collectAccount && { provenance: scored.provenance }),
5039
5254
  ...(tier && { tier }),
5040
5255
  isMethod: call.isMethod || false,
5041
5256
  ...(isFunctionReference && { isFunctionReference: true }),
@@ -5074,6 +5289,7 @@ function findCallers(index, name, options = {}) {
5074
5289
  evidenceScore: scored.evidenceScore,
5075
5290
  scoreKind: scored.scoreKind,
5076
5291
  resolution: scored.resolution,
5292
+ ...(collectAccount && { provenance: scored.provenance }),
5077
5293
  ...(tier && { tier }),
5078
5294
  });
5079
5295
  enrichedCount++;
@@ -5211,6 +5427,30 @@ function findCallees(index, definition, options = {}) {
5211
5427
  : [];
5212
5428
 
5213
5429
  const callees = new Map(); // key -> { name, bindingId, count }
5430
+ const siteEvidence = new Map();
5431
+ const provenanceForSite = (siteId, target, reason) => {
5432
+ const record = siteEvidence.get(siteId) || { call: calls[siteId], evidence: {} };
5433
+ const call = record.call;
5434
+ const evidence = { ...record.evidence, ...(reason && { reason }) };
5435
+ evidence.facts = _confirmationFacts(index, def.file, call,
5436
+ target ? [target] : (index.symbols.get(call.name) || []), record.options);
5437
+ // A bindingId synthesized by receiver resolution is not a
5438
+ // lexical binding of the method token. Rank the actual site facts.
5439
+ if (call.isMethod && !call.receiverIsModule && !call.moduleOwnedPath &&
5440
+ !evidence.typeQualifiedReceiver && !evidence.extensionMethod) evidence.hasBindingId = false;
5441
+ if (!call.isMethod && target && !evidence.hasBindingId) {
5442
+ evidence.hasImportEvidence = (index.symbols.get(call.name) || []).length === 1 ||
5443
+ target.file === def.file || index.importGraph.get(def.file)?.has(target.file);
5444
+ }
5445
+ const scored = scoreEdge(evidence);
5446
+ return {
5447
+ ...occurrenceIdentity(fileEntry?.relativePath || def.file, call, siteId),
5448
+ ...(target && { targetDef: declarationIdentity(target) }),
5449
+ confidence: scored.confidence, evidenceScore: scored.evidenceScore,
5450
+ resolution: scored.resolution, scoreKind: scored.scoreKind,
5451
+ provenance: scored.provenance,
5452
+ };
5453
+ };
5214
5454
  let selfAttrCalls = null; // collected for Python self.attr.method() resolution
5215
5455
  let selfMethodCalls = null; // collected for Python self.method() resolution
5216
5456
 
@@ -5259,7 +5499,7 @@ function findCallees(index, definition, options = {}) {
5259
5499
  };
5260
5500
  // Retain an uncertain/unresolved call as a visible unverified callee
5261
5501
  // entry (aggregated by name+reason) and claim its site.
5262
- const noteUnverified = (siteId, call, reason, meta = {}) => {
5502
+ const noteUnverified = (siteId, call, reason, meta = {}, siteProof) => {
5263
5503
  if (!collectAccount || claimedSiteIds.has(siteId)) return;
5264
5504
  noteSite(siteId, 'unverified', reason, call);
5265
5505
  const key = `${call.name}|${reason}|${meta.dispatchVia || ''}`;
@@ -5272,6 +5512,13 @@ function findCallees(index, definition, options = {}) {
5272
5512
  }
5273
5513
  entry.callCount++;
5274
5514
  entry.sites.push(call.line);
5515
+ if (!entry.siteProvenance) entry.siteProvenance = [];
5516
+ const unverifiedScore = scoreEdge({ isUncertain: true, reason });
5517
+ entry.siteProvenance.push({ ...(siteProof || provenanceForSite(siteId, null, reason)),
5518
+ tier: TIER.UNVERIFIED, reason,
5519
+ confidence: unverifiedScore.confidence, evidenceScore: unverifiedScore.evidenceScore,
5520
+ resolution: reason, scoreKind: unverifiedScore.scoreKind });
5521
+ entry.provenance = summarizeProvenance(entry.siteProvenance);
5275
5522
  };
5276
5523
  // A statically selected base implementation is not the only runtime
5277
5524
  // target in languages with virtual/structural dispatch. If a project
@@ -5353,7 +5600,8 @@ function findCallees(index, definition, options = {}) {
5353
5600
  // already carry stronger identity and must not trigger a whole-file
5354
5601
  // flow build merely because their parser record lacks receiverType.
5355
5602
  const mayNeedDirectReceiverFlow = call =>
5356
- call.isMethod && call.receiver && !call.receiverType &&
5603
+ call.isMethod && call.receiver && (!call.receiverType ||
5604
+ call.receiverTypeGuessed || call.receiverTypeSource === 'guess') &&
5357
5605
  !call.receiverPatternShadow &&
5358
5606
  !_isReservedReceiver(language, call.receiver) &&
5359
5607
  !call.isPathCall && !call.receiverIsModule &&
@@ -5378,6 +5626,14 @@ function findCallees(index, definition, options = {}) {
5378
5626
  for (let call of calls) {
5379
5627
  siteOrdinal++;
5380
5628
  const siteId = siteOrdinal;
5629
+ siteEvidence.set(siteId, {
5630
+ call,
5631
+ evidence: {
5632
+ hasReceiverType: !!call.receiverType,
5633
+ resolvedBySameClass: !!call.receiver && _isReservedReceiver(language, call.receiver),
5634
+ },
5635
+ options: { sameClass: !!call.receiver && _isReservedReceiver(language, call.receiver) },
5636
+ });
5381
5637
  if (language === 'csharp') {
5382
5638
  // fix #353: `Beta.Helper.Widget()` — namespace-qualified type
5383
5639
  // receiver (see the findCallers twin).
@@ -5391,6 +5647,8 @@ function findCallees(index, definition, options = {}) {
5391
5647
  call = {
5392
5648
  ...call,
5393
5649
  receiverType: indexedType.type,
5650
+ receiverTypeSource: 'flow',
5651
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
5394
5652
  ...(indexedType.fromFile && {
5395
5653
  receiverTypeFlowFile: indexedType.fromFile,
5396
5654
  }),
@@ -5417,6 +5675,55 @@ function findCallees(index, definition, options = {}) {
5417
5675
  if (!isDirectMatch && !isNestedCallback) continue;
5418
5676
  if (calleeAccount) calleeAccount.totalSites++;
5419
5677
 
5678
+ if (language === 'python' && call.isMethod && !call.receiverType) {
5679
+ const fixture = _pythonFixtureReceiverType(index, def.file, call);
5680
+ if (fixture) call = { ...call, receiverType: fixture.type,
5681
+ receiverTypeSource: 'fixture', receiverTypeEvidence: { source: 'fixture', ...fixture },
5682
+ ...(fixture.fromFile && { receiverTypeFlowFile: fixture.fromFile }) };
5683
+ const external = !fixture && _pythonExternalFieldFlow(index, def.file, call);
5684
+ if (external) {
5685
+ call = { ...call, receiverTypeSource: 'flow',
5686
+ receiverTypeEvidence: { source: 'flow', externalFactory: external.proof } };
5687
+ const targets = (index.symbols.get(call.name) || []).filter(d => !NON_CALLABLE_TYPES.has(d.type));
5688
+ const facts = _confirmationFacts(index, def.file, call, targets);
5689
+ const provenance = scoreEdge({ possibleDispatch: true, facts }).provenance;
5690
+ noteUnverified(siteId, call, 'possible-dispatch', { dispatchVia: external.via, externalContract: true },
5691
+ { ...occurrenceIdentity(fileEntry.relativePath, call, siteId), provenance });
5692
+ continue;
5693
+ }
5694
+ }
5695
+
5696
+ if (language === 'rust' && _rustStandardWrapperMethod(index, def.file, call, allCalls)) {
5697
+ noteSite(siteId, 'external', 'standard-wrapper-method', call);
5698
+ continue;
5699
+ }
5700
+ if (language === 'rust' && call.isMethod && !call.receiverType &&
5701
+ call.receiverPatternVariant && (collectAccount || call.receiverPatternSourceCallStart != null)) {
5702
+ const item = _rustPatternReceiverType(index, fileEntry, def.file, call, foldCtx());
5703
+ if (item?.type) call = { ...call, receiverType: item.type,
5704
+ receiverTypeSource: 'flow', receiverTypeEvidence: { source: 'flow', ...item },
5705
+ receiverTypeFlowFile: item.fromFile };
5706
+ }
5707
+ if (language === 'rust' && call.isMethod &&
5708
+ call.receiver && !call.receiverType &&
5709
+ (call.receiverIterationCall || call.receiverIterationVariable)) {
5710
+ const item = _rustIterationReceiverType(index, fileEntry, def.file, call, foldCtx());
5711
+ if (item) call = { ...call, receiverType: item.type,
5712
+ receiverTypeSource: 'flow', receiverTypeEvidence: { source: 'flow', ...item },
5713
+ receiverTypeFlowFile: item.fromFile };
5714
+ }
5715
+
5716
+ if (language === 'c' && call.isMethod) {
5717
+ noteUnverified(siteId, call, 'callable-field');
5718
+ continue;
5719
+ }
5720
+
5721
+ if (language === 'csharp' && call.receiverType === 'dynamic') {
5722
+ if (_calleeZeroCandidateName(index, call)) noteSite(siteId, 'external', null, call);
5723
+ else noteUnverified(siteId, call, 'possible-dispatch', { dispatchVia: 'dynamic' });
5724
+ continue;
5725
+ }
5726
+
5420
5727
  if (call.macroParameter) {
5421
5728
  noteSite(siteId, 'excluded', 'macro-parameter', call);
5422
5729
  continue;
@@ -5460,6 +5767,7 @@ function findCallees(index, definition, options = {}) {
5460
5767
  const selected = _calleeOverloadSelect(
5461
5768
  index, call, extensions, language);
5462
5769
  if (selected.match) {
5770
+ if (collectAccount) siteEvidence.get(siteId).evidence.extensionMethod = true;
5463
5771
  const match = selected.match;
5464
5772
  const key = match.bindingId ||
5465
5773
  `${match.file}:${match.startLine}:${call.name}`;
@@ -5530,6 +5838,8 @@ function findCallees(index, definition, options = {}) {
5530
5838
  call = {
5531
5839
  ...call,
5532
5840
  receiverType: indexedType.type,
5841
+ receiverTypeSource: 'flow',
5842
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
5533
5843
  ...(indexedType.fromFile && {
5534
5844
  receiverTypeFlowFile: indexedType.fromFile,
5535
5845
  }),
@@ -5648,6 +5958,22 @@ function findCallees(index, definition, options = {}) {
5648
5958
  }
5649
5959
  }
5650
5960
 
5961
+ if (collectAccount) {
5962
+ const record = siteEvidence.get(siteId);
5963
+ record.call = call;
5964
+ if (directReceiverFlow?.type || fieldHopType) {
5965
+ const source = directReceiverFlow?.type ? 'flow' : 'field';
5966
+ record.evidence.hasReceiverType = true;
5967
+ Object.assign(record.options, {
5968
+ receiverType: directReceiverFlow?.type || fieldHopType,
5969
+ receiverTypeSource: source,
5970
+ originFile: directReceiverFlow?.fromFile || fieldHopInfo?.fromFile,
5971
+ receiverOrigin: { source, ...(directReceiverFlow || fieldHopInfo || {}),
5972
+ field: call.receiverField, rootType: call.receiverRootType },
5973
+ });
5974
+ }
5975
+ }
5976
+
5651
5977
  if (fieldDispatchType) {
5652
5978
  noteUnverified(siteId, call, 'possible-dispatch', {
5653
5979
  dispatchVia: fieldDispatchType,
@@ -5667,6 +5993,7 @@ function findCallees(index, definition, options = {}) {
5667
5993
  const selected = _calleeOverloadSelect(
5668
5994
  index, extensionCall, extensions, language);
5669
5995
  if (selected.match) {
5996
+ if (collectAccount) siteEvidence.get(siteId).evidence.extensionMethod = true;
5670
5997
  const match = selected.match;
5671
5998
  const key = match.bindingId ||
5672
5999
  `${match.file}:${match.startLine}:${call.name}`;
@@ -5810,6 +6137,7 @@ function findCallees(index, definition, options = {}) {
5810
6137
  !_isReservedReceiver(language, call.receiver) &&
5811
6138
  !(localTypes && localTypes.has(call.receiver))) {
5812
6139
  typeQual = _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language);
6140
+ if (typeQual) siteEvidence.get(siteId).evidence.typeQualifiedReceiver = true;
5813
6141
  }
5814
6142
 
5815
6143
  // Package-qualified NON-method records (fix #268, chi-measured):
@@ -5954,7 +6282,7 @@ function findCallees(index, definition, options = {}) {
5954
6282
  }
5955
6283
  continue;
5956
6284
  }
5957
- const typeName = call.receiverType || directReceiverFlow?.type || fieldHopType;
6285
+ const typeName = directReceiverFlow?.type || call.receiverType || fieldHopType;
5958
6286
  const symbols = index.symbols.get(call.name);
5959
6287
  const qualifiedType = language === 'go' && call.receiverType
5960
6288
  ? _goQualifiedReceiverType(index, fileEntry,
@@ -5980,8 +6308,8 @@ function findCallees(index, definition, options = {}) {
5980
6308
  const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
5981
6309
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
5982
6310
  // Same-class overload selection by static call shape (fix #268)
5983
- const receiverOriginFile = call.receiverTypeFlowFile ||
5984
- directReceiverFlow?.fromFile ||
6311
+ const receiverOriginFile = directReceiverFlow?.fromFile ||
6312
+ call.receiverTypeFlowFile ||
5985
6313
  fieldHopInfo?.fromFile ||
5986
6314
  (call.receiverType
5987
6315
  ? _resolveFlowTypeOrigin(
@@ -6103,6 +6431,12 @@ function findCallees(index, definition, options = {}) {
6103
6431
  chained = _nominalChainedReceiverType(index, call, fileEntry, def.file);
6104
6432
  }
6105
6433
  if (chained?.type) {
6434
+ const record = siteEvidence.get(siteId);
6435
+ record.evidence.hasReceiverType = true;
6436
+ Object.assign(record.options, {
6437
+ receiverType: chained.type, receiverTypeSource: 'flow',
6438
+ receiverOrigin: { source: 'flow', ...chained }, originFile: chained.fromFile,
6439
+ });
6106
6440
  const symbols = index.symbols.get(call.name);
6107
6441
  const isCallableCh = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
6108
6442
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
@@ -6849,21 +7183,28 @@ function findCallees(index, definition, options = {}) {
6849
7183
  if (bound && routeVirtualOverride(siteId, call, def.className, bound)) continue;
6850
7184
  }
6851
7185
 
6852
- // Single project-wide owner (fix #236 — the caller side's
6853
- // #204/#209 rule on the callee side): an untyped-receiver method
6854
- // call whose name has exactly ONE owner type resolves to that
6855
- // owner's method — `k.run()` where only Kit defines run. Without
6856
- // it, trace trees stopped expanding at statically-resolvable
6857
- // calls the caller direction confirms.
7186
+ // Receiver evidence for the former single-owner fallback (#355).
7187
+ // A name with one owner still needs an independent typed lookup;
7188
+ // unresolved values remain visible in the contract.
6858
7189
  if (isUncertain && call.isMethod && call.receiver && !bindingResolved) {
6859
7190
  const fm = mayNeedDirectReceiverFlow(call) ? flowMap() : null;
6860
7191
  const flowEntry = fm ? _lookupReturnTypeFlow(fm, call) : undefined;
6861
7192
  const owner = _calleeSingleOwnerMatch(index, def, fileEntry, call, effectiveName, language, flowEntry);
6862
7193
  if (owner) {
7194
+ siteEvidence.get(siteId).evidence.hasSingleOwnerEvidence = true;
6863
7195
  isUncertain = false;
6864
7196
  bindingResolved = owner.bindingId;
6865
7197
  calleeKey = owner.bindingId ||
6866
7198
  `${owner.className || (owner.receiver || '').replace(/^\*/, '')}.${effectiveName}`;
7199
+ } else if (!call.receiverType && !flowEntry?.type && !fieldHopType &&
7200
+ !call.receiverExternalFlow && !call.receiverQualifiedFlow) {
7201
+ const owners = new Set((index.symbols.get(effectiveName) || [])
7202
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && (d.className || d.receiver))
7203
+ .map(d => `${d.file}:${d.className || d.receiver}`));
7204
+ if (owners.size === 1) {
7205
+ uncertainReason = 'single-owner';
7206
+ siteEvidence.get(siteId).evidence.hasSingleOwnerEvidence = true;
7207
+ }
6867
7208
  }
6868
7209
  }
6869
7210
 
@@ -6891,6 +7232,59 @@ function findCallees(index, definition, options = {}) {
6891
7232
  }
6892
7233
  }
6893
7234
 
7235
+ // Legacy callers receive no uncertainty band. Drop an untyped
7236
+ // value receiver even if a same-file or unique-name lookup picked
7237
+ // a method; only actual receiver/module/type evidence survives.
7238
+ if (!collectAccount && call.isMethod && !call.receiverType &&
7239
+ !directReceiverFlow?.type && !fieldHopType &&
7240
+ !localTypes?.has(call.receiver) &&
7241
+ !siteEvidence.get(siteId).evidence.resolvedBySameClass &&
7242
+ !siteEvidence.get(siteId).evidence.typeQualifiedReceiver &&
7243
+ !call.receiverIsModule && !call.receiverModuleSpecifier &&
7244
+ !call.receiverModuleComposition && !call.moduleOwnedPath &&
7245
+ !call.isConstructor) continue;
7246
+
7247
+ if (collectAccount) {
7248
+ const record = siteEvidence.get(siteId);
7249
+ record.call = call;
7250
+ if (bindingResolved && !record.evidence.hasSingleOwnerEvidence) {
7251
+ record.evidence.hasBindingId = true;
7252
+ record.options.bindingId = bindingResolved;
7253
+ }
7254
+ }
7255
+ if (collectAccount) {
7256
+ const record = siteEvidence.get(siteId);
7257
+ const valueReceiver = call.isMethod && !call.isConstructor &&
7258
+ !call.receiverIsModule && !call.receiverModuleSpecifier &&
7259
+ !call.receiverModuleComposition && !call.moduleOwnedPath &&
7260
+ !record.evidence.typeQualifiedReceiver && !record.evidence.extensionMethod &&
7261
+ !record.evidence.resolvedBySameClass;
7262
+ if (valueReceiver) {
7263
+ // Resolve and classify THIS occurrence before its grouping
7264
+ // key is chosen. A sibling site cannot lend its receiver
7265
+ // evidence or overload identity to this one.
7266
+ const proof = provenanceForSite(siteId, null);
7267
+ const facts = proof.provenance.facts;
7268
+ const checked = validateConfirmation(proof.provenance, facts.targets);
7269
+ const selected = checked.verdict === 'establishes-target' &&
7270
+ (index.symbols.get(effectiveName) || []).find(candidate =>
7271
+ sameDeclaration(declarationIdentity(candidate), facts.lookup?.selected));
7272
+ const reportOnly = checked.verdict === 'unsupported' && record.evidence.hasReceiverType;
7273
+ if (!selected && !reportOnly) {
7274
+ const reason = !record.evidence.hasReceiverType && facts.ownerCount === 1
7275
+ ? 'single-owner' : 'provenance-incomplete';
7276
+ if (reason === 'single-owner') proof.provenance = scoreEdge({
7277
+ hasSingleOwnerEvidence: true, facts,
7278
+ }).provenance;
7279
+ noteUnverified(siteId, call, reason, {}, proof);
7280
+ continue;
7281
+ }
7282
+ if (selected) {
7283
+ bindingResolved = selected.bindingId;
7284
+ calleeKey = selected.bindingId || `${selected.file}:${selected.startLine}:${selected.name}`;
7285
+ }
7286
+ }
7287
+ }
6894
7288
  const existing = callees.get(calleeKey);
6895
7289
  if (existing) {
6896
7290
  existing.count += 1;
@@ -7254,24 +7648,61 @@ function findCallees(index, definition, options = {}) {
7254
7648
  }
7255
7649
  }
7256
7650
 
7257
- const calleeScored = scoreEdge({
7651
+ let calleeScored = scoreEdge({
7258
7652
  hasBindingId: !!bindingId,
7259
7653
  hasImportEvidence: !!bindingId || resolutionSymbols.length === 1 ||
7260
7654
  (callee.file === def.file) || callerImportSet.has(callee.file),
7261
7655
  isUncertain: false, // uncertain callees already filtered above
7262
7656
  });
7657
+ const siteProvenance = collectAccount ? siteIds.map(siteId => ({
7658
+ ...provenanceForSite(siteId, callee), tier: TIER.CONFIRMED,
7659
+ })).sort((a, b) => a.siteId - b.siteId).filter(site => {
7660
+ const record = siteEvidence.get(site.siteId);
7661
+ const valueReceiver = record.call.isMethod &&
7662
+ !record.call.receiverIsModule && !record.call.receiverModuleSpecifier &&
7663
+ !record.call.receiverModuleComposition && !record.evidence.typeQualifiedReceiver;
7664
+ const untypedValue = valueReceiver && !record.evidence.hasReceiverType &&
7665
+ !record.evidence.resolvedBySameClass;
7666
+ if (untypedValue && site.provenance.facts.ownerCount === 1) {
7667
+ record.evidence.hasSingleOwnerEvidence = true;
7668
+ }
7669
+ const migrated = valueReceiver && !record.call.isConstructor &&
7670
+ !record.evidence.resolvedBySameClass && !record.evidence.extensionMethod &&
7671
+ (record.evidence.hasReceiverType || record.evidence.hasSingleOwnerEvidence || untypedValue);
7672
+ if (!migrated) return true;
7673
+ const checked = validateConfirmation(site.provenance, [declarationIdentity(callee)]);
7674
+ if (checked.verdict === 'establishes-target' ||
7675
+ (checked.verdict === 'unsupported' && record.evidence.hasReceiverType)) return true;
7676
+ if (checked.verdict === 'establishes-other') {
7677
+ noteSite(site.siteId, 'excluded', 'receiver-target-different', record.call);
7678
+ (calleeAccount.excluded.evidence ||= []).push(site);
7679
+ } else {
7680
+ const reason = record.evidence.hasSingleOwnerEvidence && !record.evidence.hasReceiverType
7681
+ ? 'single-owner' : 'provenance-incomplete';
7682
+ if (reason === 'single-owner') site.provenance = scoreEdge({
7683
+ hasSingleOwnerEvidence: true, facts: site.provenance.facts,
7684
+ }).provenance;
7685
+ noteUnverified(site.siteId, record.call, reason, {}, site);
7686
+ }
7687
+ return false;
7688
+ }) : null;
7689
+ if (collectAccount && siteProvenance.length === 0) continue;
7690
+ if (collectAccount) calleeScored = siteProvenance.reduce((weakest, site) =>
7691
+ site.evidenceScore < weakest.evidenceScore ? site : weakest);
7263
7692
  claimSites('confirmed', null);
7264
7693
  result.push({
7265
7694
  ...callee,
7266
- callCount: count,
7267
- weight: index.calculateWeight(count),
7695
+ callCount: collectAccount ? siteProvenance.length : count,
7696
+ weight: index.calculateWeight(collectAccount ? siteProvenance.length : count),
7268
7697
  confidence: calleeScored.confidence,
7269
7698
  evidenceScore: calleeScored.evidenceScore,
7270
7699
  scoreKind: calleeScored.scoreKind,
7271
7700
  resolution: calleeScored.resolution,
7272
7701
  ...(collectAccount && {
7273
7702
  tier: TIER.CONFIRMED,
7274
- sites: [...sites].sort((a, b) => a - b),
7703
+ provenance: summarizeProvenance(siteProvenance),
7704
+ siteProvenance,
7705
+ sites: siteProvenance.map(site => site.line).sort((a, b) => a - b),
7275
7706
  ...(isFunctionReference && { functionReference: true }),
7276
7707
  }),
7277
7708
  });
@@ -7369,6 +7800,28 @@ function getInstanceAttributeTypes(index, filePath, className) {
7369
7800
  'python', { filePath, consumerAwaited: false });
7370
7801
  return result?.fromFile ? result.type : null;
7371
7802
  },
7803
+ resolveImportedCallType(name) {
7804
+ const bindings = (fileEntry.importBindings || []).filter(binding =>
7805
+ (binding.alias || binding.name) === name);
7806
+ if (bindings.length !== 1 || (fileEntry.moduleAssignedNames || []).includes(name) ||
7807
+ (index.symbols.get(name) || []).some(d => d.file === filePath)) return null;
7808
+ const binding = bindings[0];
7809
+ if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) return null;
7810
+ const type = langModule.getBuiltinCallReturnType?.(binding.module, binding.name);
7811
+ return type ? { type, binding: { ...binding }, externalModule: binding.module } : null;
7812
+ },
7813
+ resolveExternalFactory(parts) {
7814
+ const name = parts[0];
7815
+ const bindings = (fileEntry.importBindings || []).filter(binding => (binding.alias || binding.name) === name);
7816
+ if (bindings.length !== 1 || (fileEntry.moduleAssignedNames || []).includes(name) ||
7817
+ (index.symbols.get(name) || []).some(d => d.file === filePath)) return null;
7818
+ const binding = bindings[0];
7819
+ if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) return null;
7820
+ const importedFunction = ['from', 'relative'].includes(binding.kind);
7821
+ if (parts.length !== (importedFunction ? 1 : 2)) return null;
7822
+ return { module: binding.module, producer: importedFunction ? binding.name : parts[1],
7823
+ binding: { ...binding }, projectDeclarations: [], projectBindings: [], resolvedModule: null };
7824
+ },
7372
7825
  });
7373
7826
  index._attrTypeCache.set(filePath, fileCache);
7374
7827
  } catch {
@@ -7616,6 +8069,30 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7616
8069
  candidate.callStart === call.receiverCallStart &&
7617
8070
  candidate.callEnd === call.receiverCallEnd);
7618
8071
 
8072
+ // A separate unwrap/expect assignment consumes the payload of the
8073
+ // receiver's declared standard wrapper. Its spelling alone is not a
8074
+ // contract: project-defined Result/Option classes remain ordinary
8075
+ // method producers. Reassignments are checked by the same nearest-
8076
+ // assignment lookup used for every other flow receiver.
8077
+ if (language === 'rust' && call.isMethod && call.receiver &&
8078
+ !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
8079
+ ((call.name === 'unwrap' && call.argCount === 0) ||
8080
+ (call.name === 'expect' && call.argCount === 1))) {
8081
+ const receiverFlow = _lookupReturnTypeFlow(map, call);
8082
+ const contract = receiverFlow?.rustWrapper;
8083
+ if (contract?.type && validateRustWrapperContract(contract)) {
8084
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
8085
+ const key = `${scope}:${call.assignedTo}`;
8086
+ if (!map.has(key)) map.set(key, []);
8087
+ map.get(key).push({ line: call.line, start: call.callStart,
8088
+ type: contract.type, fromFile: contract.fromFile,
8089
+ wrapperUnwrap: { method: call.name,
8090
+ producer: { line: receiverFlow.line, start: receiverFlow.start,
8091
+ type: receiverFlow.type, fromFile: receiverFlow.fromFile }, contract } });
8092
+ continue;
8093
+ }
8094
+ }
8095
+
7619
8096
  // Rust's `collect::<Vec<Item>>()` turbofish fixes the concrete result
7620
8097
  // and its item type at the call site. Preserve the item identity even
7621
8098
  // though the outer collection is a standard-library type with no
@@ -7692,7 +8169,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7692
8169
  ? resolved[0]
7693
8170
  : null;
7694
8171
  }
7695
- if (folded?.type || folded?.externalVia) {
8172
+ if (folded?.type || folded?.externalVia || folded?.rustWrapper) {
7696
8173
  const scope = call.enclosingFunction
7697
8174
  ? `${call.enclosingFunction.startLine}` : '';
7698
8175
  const key = `${scope}:${call.assignedTo}`;
@@ -7701,12 +8178,15 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7701
8178
  map.get(key).push({ line: call.line, start: call.callStart,
7702
8179
  ...(folded.type && { type: folded.type }),
7703
8180
  ...(folded.fromFile && { fromFile: folded.fromFile }),
8181
+ ...(folded.rustWrapper && { rustWrapper: folded.rustWrapper }),
8182
+ ...(folded.rustReturnDeclaration && { rustReturnDeclaration: folded.rustReturnDeclaration }),
8183
+ ...(folded.moduleProducer && { moduleProducer: folded.moduleProducer }),
7704
8184
  ...(folded.externalVia && { externalVia: folded.externalVia }),
7705
8185
  ...(folded.externalConcrete && { externalConcrete: true }) });
7706
8186
  continue;
7707
8187
  }
7708
8188
  }
7709
- let returnType, fromFile, selfClass, returnedFunctionResult, returnDefinition;
8189
+ let returnType, fromFile, selfClass, returnedFunctionResult, returnDefinition, moduleProducer;
7710
8190
  const builtinCallReturn = !nominal && language === 'python'
7711
8191
  ? _pythonBuiltinCallReturnType(index, fileEntry, call) : null;
7712
8192
  if (call.localValueCall && call.returnTypeHint) {
@@ -7732,10 +8212,14 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7732
8212
  returnType = callableFlow.returnedFunctionResult;
7733
8213
  fromFile = callableFlow.fromFile;
7734
8214
  } else if (call.isMethod && call.receiverType &&
7735
- !call.receiverTypeGuessed) {
8215
+ !call.receiverTypeGuessed && call.receiverTypeSource !== 'guess') {
7736
8216
  const defs = index.symbols.get(call.name) || [];
7737
8217
  if (nominal) {
7738
- const matches = defs.filter(d => d.className === call.receiverType && d.returnType);
8218
+ const rustOwner = language === 'rust'
8219
+ ? _rustFlowReceiverOrigin(index, filePath, call.receiverType, call.receiverTypeQualifier)
8220
+ : null;
8221
+ const matches = defs.filter(d => d.className === call.receiverType && d.returnType &&
8222
+ (language !== 'rust' || rustOwner?.fromFile));
7739
8223
  if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
7740
8224
  returnType = matches[0].returnType;
7741
8225
  fromFile = matches[0].file;
@@ -7846,6 +8330,15 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7846
8330
  cls = next;
7847
8331
  }
7848
8332
  } else if (nominal && call.isMethod && call.isPathCall && call.receiver) {
8333
+ if (language === 'rust') {
8334
+ const producer = _rustModuleProducer(index, fileEntry, filePath, call);
8335
+ if (producer) {
8336
+ returnDefinition = producer.definition;
8337
+ returnType = returnDefinition.returnType;
8338
+ fromFile = returnDefinition.file;
8339
+ moduleProducer = producer.facts;
8340
+ }
8341
+ }
7849
8342
  if (language === 'cpp') {
7850
8343
  // C++ value-initialization through a qualified type name:
7851
8344
  // `auto p = fmt::pipe()`. The portable tree-sitter AST uses
@@ -8327,6 +8820,18 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
8327
8820
  }
8328
8821
  const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, parsed.name, parsed.qualifier);
8329
8822
  if (!origin) {
8823
+ const wrapper = language === 'rust' && !call.assignedUnwrap &&
8824
+ _rustReturnWrapper(index, returnType, fromFile || filePath, selfClass,
8825
+ returnDefinition, [], true);
8826
+ if (wrapper && validateRustWrapperContract(wrapper, true)) {
8827
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
8828
+ const key = `${scope}:${call.assignedTo}`;
8829
+ if (!map) map = new Map();
8830
+ if (!map.has(key)) map.set(key, []);
8831
+ map.get(key).push({ line: call.line, start: call.callStart,
8832
+ rustWrapper: wrapper, rustReturnDeclaration: _rustReturnDeclaration(returnDefinition) });
8833
+ continue;
8834
+ }
8330
8835
  // A project producer can return a package-qualified external
8331
8836
  // Go type (`DefaultLogger(...) http.Handler`). That still
8332
8837
  // gives compiler-grade provenance: the assigned receiver is
@@ -8373,6 +8878,12 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
8373
8878
  if (!map.has(key)) map.set(key, []);
8374
8879
  map.get(key).push({ line: call.line, start: call.callStart, type: typeName,
8375
8880
  ...(entryFromFile && { fromFile: entryFromFile }),
8881
+ ...(moduleProducer && { moduleProducer }),
8882
+ ...(language === 'rust' && returnDefinition && { rustReturnDeclaration: _rustReturnDeclaration(returnDefinition) }),
8883
+ ...(language === 'rust' && !call.assignedUnwrap && {
8884
+ rustWrapper: _rustReturnWrapper(index, returnType, fromFile || filePath,
8885
+ selfClass, returnDefinition),
8886
+ }),
8376
8887
  ...(iteratorItemType && {
8377
8888
  iteratorItemType,
8378
8889
  iteratorItemFromFile,
@@ -8480,7 +8991,15 @@ function _lookupReturnTypeFlow(map, call) {
8480
8991
  for (const e of entries) {
8481
8992
  if (precedesCall(e) && laterThan(e, best)) best = e;
8482
8993
  }
8483
- if (best) return best.invalidated ? undefined : best;
8994
+ if (best) {
8995
+ const binding = call.receiverTypeEvidence?.aliasBinding;
8996
+ // A non-call copy/rebinding is absent from the producer map.
8997
+ // Its newer AST declaration must still invalidate an older
8998
+ // factory payload with the same local variable name.
8999
+ if (binding?.target === call.receiver && Number.isInteger(binding.assignment?.start) &&
9000
+ (best.start == null || binding.assignment.start > best.start)) return undefined;
9001
+ return best.invalidated ? undefined : best;
9002
+ }
8484
9003
  }
8485
9004
  return undefined;
8486
9005
  }
@@ -8555,6 +9074,59 @@ function _splitTopLevelGenericArgs(s) {
8555
9074
  return out;
8556
9075
  }
8557
9076
 
9077
+ function _rustReturnWrapper(index, text, file, selfClass, producer, projection = [], allowUnknownPayload = false) {
9078
+ if (!text || !file || !producer) return null;
9079
+ const contract = rustWrapperContract(index, text, file, {
9080
+ projection,
9081
+ allowUnknownPayload,
9082
+ parseType: value => _returnTypeNameNominal(value, 'rust', { selfClass }),
9083
+ resolveType: (context, name, qualifier) => _resolveFlowTypeOrigin(index, context, name, qualifier),
9084
+ });
9085
+ return contract && { ...contract, producer: { ...declarationIdentity(producer), returnType: producer.returnType } };
9086
+ }
9087
+
9088
+ function _rustReturnDeclaration(definition) {
9089
+ return { ...declarationIdentity(definition), file: definition.file,
9090
+ relativePath: definition.relativePath, returnType: definition.returnType };
9091
+ }
9092
+
9093
+ function _rustStandardWrapperMethod(index, file, call, calls) {
9094
+ if (!call.isMethod || !call.receiver || call.isPathCall ||
9095
+ call.receiverPatternShadow || call.receiverFlowInvalidated ||
9096
+ !['unwrap', 'expect'].includes(call.name)) return null;
9097
+ const flow = _lookupReturnTypeFlow(_buildReturnTypeFlowMap(index, file, calls), call);
9098
+ const contract = flow?.rustWrapper;
9099
+ if (contract && validateRustWrapperContract(contract, true)) {
9100
+ return { method: call.name, receiver: call.receiver, callKind: 'method',
9101
+ producer: { line: flow.line, start: flow.start }, contract };
9102
+ }
9103
+ // Ownership of a standard wrapper does not require knowing its payload:
9104
+ // `value: Result<T, E>` still uses Result's inherent unwrap. This negative
9105
+ // proof never types T or attributes its later methods to a project class.
9106
+ const type = call.receiverType;
9107
+ if (!['Result', 'Option'].includes(type) || call.receiverTypeSource !== 'annotation' ||
9108
+ call.receiverTypeEvidence?.source !== 'annotation' ||
9109
+ _isGenericParamReceiverType(index, file, call.line, type)) return null;
9110
+ const entry = index.files.get(file);
9111
+ const module = type === 'Result' ? 'result' : 'option';
9112
+ const standardPaths = [`std::${module}::${type}`, `core::${module}::${type}`];
9113
+ const qualifier = call.receiverTypeQualifier;
9114
+ const local = (index.symbols.get(type) || []).filter(d => d.file === file &&
9115
+ (IDENTITY_TYPE_KINDS.has(d.type) || d.type === 'type'));
9116
+ const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === type);
9117
+ if (qualifier ? !standardPaths.includes(`${qualifier}::${type}`)
9118
+ : local.length || bindings.some(b => !standardPaths.includes(b.module))) return null;
9119
+ if ((entry.importBindings || []).some(b => b.name === '*' || b.module?.endsWith('::*'))) return null;
9120
+ const root = qualifier?.split('::')[0] || bindings[0]?.module.split('::')[0];
9121
+ if (root && ((index.symbols.get(root) || []).some(d => d.file === file) ||
9122
+ (entry.importBindings || []).some(b => (b.alias || b.name) === root))) return null;
9123
+ return { method: call.name, receiver: call.receiver, callKind: 'method',
9124
+ annotationReceiver: { type, origin: call.receiverTypeEvidence,
9125
+ qualifier: qualifier || null, localDeclarations: local.map(declarationIdentity),
9126
+ bindings, wildcardImports: [], rootDeclarations: [], rootBindings: [],
9127
+ genericParameter: false } };
9128
+ }
9129
+
8558
9130
  function _splitTopLevelDelimiter(s, delimiter) {
8559
9131
  const out = [];
8560
9132
  let angle = 0, paren = 0, bracket = 0, brace = 0, cur = '';
@@ -8659,10 +9231,11 @@ function _returnTypeNameNominal(text, language, opts = {}) {
8659
9231
  if (language === 'go') {
8660
9232
  const qm = t.replace(/^\*+/, '').match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
8661
9233
  if (qm) qualifier = qm[1];
8662
- } else if (language === 'cpp') {
8663
- const stripped = t
8664
- .replace(/\b(const|volatile|class|struct|typename)\b/g, '')
8665
- .replace(/[*&]+/g, '').trim();
9234
+ } else if (language === 'cpp' || language === 'rust') {
9235
+ const stripped = language === 'rust'
9236
+ ? t.replace(/^&(?:\s*'\w+)?\s*/, '').replace(/^mut\s+/, '').trim()
9237
+ : t.replace(/\b(const|volatile|class|struct|typename)\b/g, '')
9238
+ .replace(/[*&]+/g, '').trim();
8666
9239
  const genericStart = stripped.indexOf('<');
8667
9240
  const head = genericStart >= 0
8668
9241
  ? stripped.slice(0, genericStart).trim() : stripped;
@@ -8807,6 +9380,35 @@ function _rustImportedTypeIdentity(index, filePath, localName) {
8807
9380
  * trusted (a use/import of an external type can shadow it invisibly)
8808
9381
  * - no project type def at all: external name — safe, can't conflate
8809
9382
  */
9383
+ function _rustFlowReceiverOrigin(index, file, name, qualifier) {
9384
+ const entry = index.files.get(file);
9385
+ // An annotation names a type in this module's scope. A same-directory
9386
+ // project Result/Option declaration does not shadow the standard prelude
9387
+ // of another module, nor does an unrelated imported file bind its names.
9388
+ const local = (index.symbols.get(name) || []).some(d => d.file === file &&
9389
+ (IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf)));
9390
+ const bound = (entry?.importBindings || []).some(b => (b.alias || b.name) === name);
9391
+ if (!qualifier && !local && !bound) return null;
9392
+ return _resolveFlowTypeOrigin(index, file, name, qualifier);
9393
+ }
9394
+
9395
+ function _rustSameNameAliasOrigin(index, definition, seen = new Set()) {
9396
+ if (!definition || seen.has(definition) || seen.size >= 8) return null;
9397
+ seen.add(definition);
9398
+ if (definition.type !== 'type') return { fromFile: definition.file };
9399
+ if (!definition.aliasTypeText) return null;
9400
+ const parsed = _returnTypeNameNominal(definition.aliasTypeText, 'rust');
9401
+ if (parsed?.name !== definition.name || !parsed.qualifier) return null;
9402
+ const relative = index.files.get(definition.file)?.moduleResolved?.[parsed.qualifier];
9403
+ const resolved = relative ? path.resolve(index.root, relative)
9404
+ : resolveRustImport(`${parsed.qualifier}::${parsed.name}`, definition.file, index.root) ||
9405
+ resolveRustImport(parsed.qualifier, definition.file, index.root);
9406
+ if (!resolved) return null;
9407
+ const targets = (index.symbols.get(parsed.name) || []).filter(d =>
9408
+ IDENTITY_TYPE_KINDS.has(d.type) && d.file === resolved);
9409
+ return targets.length === 1 ? _rustSameNameAliasOrigin(index, targets[0], seen) : null;
9410
+ }
9411
+
8810
9412
  function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undefined) {
8811
9413
  const opCache = index._opFlowTypeOriginCache;
8812
9414
  const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
@@ -8863,6 +9465,13 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undef
8863
9465
  const resolved = resolveRustImport(
8864
9466
  `${qualifier}::${typeName}`, producerFile, index.root);
8865
9467
  if (resolved) {
9468
+ const local = typeDefs.filter(d => d.file === resolved);
9469
+ if (local.length === 1) {
9470
+ const direct = local[0].type === 'type' && local[0].aliasOf === typeName
9471
+ ? _rustSameNameAliasOrigin(index, local[0])
9472
+ : { fromFile: local[0].file };
9473
+ if (direct) return finish(direct);
9474
+ }
8866
9475
  const reachable = typeDefs.filter(d =>
8867
9476
  d.file === resolved ||
8868
9477
  _importReaches(index, resolved, new Set([d.file])));
@@ -9052,22 +9661,7 @@ const JS_GLOBAL_RECEIVERS = new Set([
9052
9661
  'crypto', 'performance', 'history', 'location', 'screen',
9053
9662
  ]);
9054
9663
 
9055
- const BUILTIN_RECEIVER_TYPES = new Set([
9056
- 'dict', 'list', 'set', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'frozenset',
9057
- 'Mapping', 'MutableMapping', 'Sequence', 'MutableSequence',
9058
- 'Collection', 'Iterable', 'Iterator', 'KeysView', 'ValuesView', 'ItemsView',
9059
- 'IO', 'TextIO', 'BinaryIO', 'StringIO', 'BytesIO',
9060
- 'ZlibCompress', 'ZlibDecompress',
9061
- 'AsyncEvent',
9062
- 'Generator', 'AsyncGenerator', 'ContextManager', 'AsyncContextManager',
9063
- 'Array', 'String', 'Object', 'RegExp', 'Number', 'Boolean', 'Map', 'Set', 'Promise',
9064
- 'WeakMap', 'WeakSet',
9065
- 'string', 'number', 'boolean', 'bigint', 'symbol',
9066
- 'object', 'dynamic', 'decimal', 'byte', 'sbyte', 'char',
9067
- 'short', 'ushort', 'uint', 'long', 'ulong', 'double',
9068
- 'List', 'Dictionary', 'HashSet', 'Queue', 'Stack',
9069
- 'Task', 'ValueTask', 'IEnumerable', 'ICollection', 'IList',
9070
- ]);
9664
+
9071
9665
 
9072
9666
  // Universal-contract method names (fix #265, hono-measured: 183 untyped
9073
9667
  // `x.toString()` calls confirmed against JSXNode.toString via the single-
@@ -10959,6 +11553,60 @@ function _pythonBuiltinContractAllowed(index, fileEntry, moduleName) {
10959
11553
  return !_unresolvedModuleIsGap(index, module);
10960
11554
  }
10961
11555
 
11556
+ function _pythonExternalFieldFlow(index, file, call) {
11557
+ const field = call.receiverRoot === 'self' && (!call.receiverFields || call.receiverFields.length === 1)
11558
+ ? call.receiverField : call.receiver?.startsWith('self.') && call.receiver.split('.').length === 2
11559
+ ? call.receiver.slice(5) : null;
11560
+ if (!field || call.receiverFlowInvalidated) return null;
11561
+ const enclosing = index.findEnclosingFunction(file, call.line, true);
11562
+ if (!enclosing?.className) return null;
11563
+ const owner = (index.symbols.get(enclosing.className) || []).filter(d => d.file === file && d.type === 'class');
11564
+ if (owner.length !== 1) return null;
11565
+ const flow = getInstanceAttributeTypes(index, file, enclosing.className)?.externalFlows?.get(field);
11566
+ if (!flow) return null;
11567
+ const producer = flow.assignments[0];
11568
+ return { via: `${producer.module}.${producer.producer}${'()'.repeat(producer.depth)}`,
11569
+ proof: { ...flow, owner: declarationIdentity(owner[0]), enclosing: declarationIdentity(enclosing) } };
11570
+ }
11571
+
11572
+ function _pythonFixtureReceiverType(index, file, call) {
11573
+ const resolveType = (context, name, qualifier) => {
11574
+ const root = qualifier || name, entry = index.files.get(context);
11575
+ if ((entry.moduleAssignedNames || []).includes(root) ||
11576
+ (entry.importBindings || []).filter(b => (b.alias || b.name) === root).length > 1 ||
11577
+ (qualifier && (index.symbols.get(root) || []).some(d => d.file === context))) return null;
11578
+ return pythonFixtureType(index, context, qualifier ? `${qualifier}.${name}` : name,
11579
+ d => IDENTITY_TYPE_KINDS.has(d.type));
11580
+ };
11581
+ return pythonFixtureReceiver(index, file, call, {
11582
+ resolveType,
11583
+ externalModule: (context, module) => _pythonBuiltinContractAllowed(index, index.files.get(context), module),
11584
+ field(type, context, field) {
11585
+ const owner = resolveType(context, type)?.declaration;
11586
+ if (!owner) return null;
11587
+ const members = (index.symbols.get(field) || []).filter(d => d.className === type && d.file === owner.file &&
11588
+ ['property', 'getter', 'field'].includes(d.type));
11589
+ if (members.length !== 1) return null;
11590
+ const member = members[0], text = member.returnType || member.fieldType;
11591
+ const result = _structuralTypeExpression(text);
11592
+ if (!result || result.args.length) return null;
11593
+ const qualifier = result.qualifiedHead.includes('.')
11594
+ ? result.qualifiedHead.slice(0, result.qualifiedHead.lastIndexOf('.')) : undefined;
11595
+ const next = resolveType(member.file, result.head, qualifier);
11596
+ const builtin = !qualifier && !next && isProvenanceBuiltinReceiver(result.head, 'python') &&
11597
+ !(index.files.get(member.file).importBindings || []).some(b => (b.alias || b.name) === result.head) &&
11598
+ !(index.files.get(member.file).moduleAssignedNames || []).includes(result.head) &&
11599
+ !(index.symbols.get(result.head) || []).some(d => d.file === member.file);
11600
+ if (!next && !builtin) return null;
11601
+ return { type: result.head, fromFile: next?.declaration.file,
11602
+ fact: { owner: declarationIdentity(owner), member: declarationIdentity(member), annotation: text,
11603
+ result: next ? declarationIdentity(next.declaration) : { builtin: result.head, language: 'python' },
11604
+ ...(builtin && { builtinShadowDeclarations: [], builtinShadowBindings: [] }),
11605
+ importChain: next?.chain || [] } };
11606
+ },
11607
+ });
11608
+ }
11609
+
10962
11610
  function _structuralImportedReceiverType(index, fileEntry, receiver) {
10963
11611
  const bindings = (fileEntry.importBindings || []).filter(binding =>
10964
11612
  binding.name === receiver || binding.alias === receiver);
@@ -10981,7 +11629,7 @@ function _structuralImportedReceiverType(index, fileEntry, receiver) {
10981
11629
  return { type: receiver, fromFile: definition.file };
10982
11630
  }
10983
11631
 
10984
- function _pythonBuiltinCallReturnType(index, fileEntry, call) {
11632
+ function _pythonBuiltinCallReturnType(index, fileEntry, call, evidence = null) {
10985
11633
  if (fileEntry?.language !== 'python') return null;
10986
11634
  const adapter = getLanguageAdapter('python');
10987
11635
  if (typeof adapter?.getBuiltinCallReturnType !== 'function') return null;
@@ -10998,7 +11646,10 @@ function _pythonBuiltinCallReturnType(index, fileEntry, call) {
10998
11646
  for (const binding of bindings) {
10999
11647
  if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) continue;
11000
11648
  const type = adapter.getBuiltinCallReturnType(binding.module, call.name);
11001
- if (type) types.add(type);
11649
+ if (type) {
11650
+ types.add(type);
11651
+ if (evidence) (evidence.bindings ||= []).push({ ...binding, returnType: type });
11652
+ }
11002
11653
  }
11003
11654
  return types.size === 1 ? [...types][0] : null;
11004
11655
  }
@@ -11010,21 +11661,30 @@ function _pythonBuiltinFieldPathType(index, fileEntry, root, fields) {
11010
11661
  const bindings = (fileEntry.importBindings || []).filter(binding =>
11011
11662
  binding.name === root || binding.alias === root);
11012
11663
  const types = new Set();
11664
+ const contracts = [];
11013
11665
  for (const binding of bindings) {
11014
11666
  if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) continue;
11015
11667
  const type = adapter.getBuiltinFieldType(binding.module, fields[0]);
11016
- if (type) types.add(type);
11668
+ if (type) {
11669
+ types.add(type);
11670
+ contracts.push({ ...binding, field: fields[0], type });
11671
+ }
11017
11672
  }
11018
- return types.size === 1 ? [...types][0] : null;
11673
+ return types.size === 1 ? { type: [...types][0], root, fields, bindings: contracts } : null;
11019
11674
  }
11020
11675
 
11021
11676
  function _pythonBuiltinChainedReceiverType(index, fileEntry, call, foldCtx) {
11022
11677
  if (fileEntry?.language !== 'python') return null;
11023
11678
  const producers = _chainedProducerRecords(foldCtx, call);
11024
11679
  if (producers.length === 0) return null;
11025
- const types = producers.map(producer =>
11026
- _pythonBuiltinCallReturnType(index, fileEntry, producer));
11027
- return types.every(Boolean) && new Set(types).size === 1 ? types[0] : null;
11680
+ const witnesses = producers.map(producer => {
11681
+ const evidence = { name: producer.name, line: producer.line, site: producer.callSite };
11682
+ const type = _pythonBuiltinCallReturnType(index, fileEntry, producer, evidence);
11683
+ return { type, ...evidence };
11684
+ });
11685
+ const types = witnesses.map(witness => witness.type);
11686
+ return types.every(Boolean) && new Set(types).size === 1
11687
+ ? { type: types[0], producers: witnesses } : null;
11028
11688
  }
11029
11689
 
11030
11690
  /**
@@ -11127,6 +11787,7 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
11127
11787
  let unknown = false;
11128
11788
  let frontier = [[startAbs, exposedName]];
11129
11789
  const shapeMatches = d =>
11790
+ (language !== 'rust' || !(d.className || d.receiver)) &&
11130
11791
  (!NON_CALLABLE_TYPES.has(d.type) ||
11131
11792
  (call.isConstructor && d.type === 'class') ||
11132
11793
  (langTraits(language)?.classesCallableWithoutNew && d.type === 'class')) &&
@@ -11264,6 +11925,19 @@ function _calleeGoPackageMatch(index, call, importModule) {
11264
11925
  return bestMatch;
11265
11926
  }
11266
11927
 
11928
+ function selectProvenanceOverload(index, call, candidates, language) {
11929
+ const remaining = new Set(candidates);
11930
+ const slots = [];
11931
+ for (const definition of candidates) {
11932
+ if (!remaining.has(definition)) continue;
11933
+ const group = _closeCallableIdentityGroup(index, [definition], candidates);
11934
+ const representative = group.find(d => !d.isSignature && !d.isDeclaration) || definition;
11935
+ slots.push(representative);
11936
+ for (const member of group) remaining.delete(member);
11937
+ }
11938
+ return _calleeOverloadSelect(index, call, slots, language);
11939
+ }
11940
+
11267
11941
  function _calleeOverloadSelect(index, call, matches, language) {
11268
11942
  if (matches.length === 0) return { match: null };
11269
11943
  if (call.argCount == null || call.argSpread) {
@@ -11580,6 +12254,7 @@ function _declaredFieldType(
11580
12254
  className: rootType,
11581
12255
  fieldType,
11582
12256
  file: owner.file,
12257
+ ...(attrs.origins?.get(fieldName) && { fieldOrigin: attrs.origins.get(fieldName) }),
11583
12258
  });
11584
12259
  }
11585
12260
  if (complete && inferred.length > 0) {
@@ -11642,6 +12317,7 @@ function _declaredFieldType(
11642
12317
  const aliasBase = _pureAliasBase(index, typeName);
11643
12318
  if (aliasBase) typeName = aliasBase;
11644
12319
  if (info) {
12320
+ if (onType.length === 1 && onType[0].fieldOrigin) Object.assign(info, onType[0].fieldOrigin);
11645
12321
  const origins = new Set();
11646
12322
  const namespaces = new Set();
11647
12323
  let complete = true;
@@ -12309,7 +12985,14 @@ function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, fl
12309
12985
  if (matchFe && callerFe &&
12310
12986
  (isTestFile(matchFe.relativePath, matchFe.language) || isTestPath(matchFe.relativePath)) &&
12311
12987
  !(isTestFile(callerFe.relativePath, callerFe.language) || isTestPath(callerFe.relativePath))) return null;
12312
- return match;
12988
+ const facts = _confirmationFacts(index, def.file, call, [match], {
12989
+ ...(flowEntry?.type && !call.receiverType && {
12990
+ receiverType: flowEntry.type, receiverTypeSource: 'flow',
12991
+ receiverOrigin: { source: 'flow', ...flowEntry }, originFile: flowEntry.fromFile,
12992
+ }),
12993
+ });
12994
+ return validateConfirmation({ facts }, facts.targets).verdict === 'establishes-target'
12995
+ ? match : null;
12313
12996
  }
12314
12997
 
12315
12998
  const _CSHARP_TYPE_ALIASES = new Map([
@@ -15111,8 +15794,18 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
15111
15794
  const parsed = _returnTypeNameNominal(def.returnType, language, { selfClass: selfType });
15112
15795
  if (!parsed) return null;
15113
15796
  const origin = _resolveFlowTypeOrigin(index, def.file || opts.filePath, parsed.name, parsed.qualifier);
15114
- if (!origin) return null;
15115
- return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
15797
+ const wrappers = language === 'rust' ? owned.map(candidate =>
15798
+ _rustReturnWrapper(index, candidate.returnType, candidate.file || opts.filePath,
15799
+ selfType, candidate, [], true)) : [];
15800
+ // Identical annotation text in different impl files can bind different
15801
+ // aliases. Every possible producer must agree on the payload identity.
15802
+ const rustWrapper = wrappers.length && wrappers.every(wrapper => wrapper &&
15803
+ wrapper.kind === wrappers[0].kind && wrapper.type === wrappers[0].type &&
15804
+ wrapper.fromFile === wrappers[0].fromFile) ? wrappers[0] : null;
15805
+ if (!origin && !rustWrapper) return null;
15806
+ return { ...(origin && { type: parsed.name }), ...(origin?.fromFile && { fromFile: origin.fromFile }),
15807
+ ...(language === 'rust' && owned.length === 1 && { rustReturnDeclaration: _rustReturnDeclaration(def) }),
15808
+ ...(rustWrapper && { rustWrapper }) };
15116
15809
  }
15117
15810
  // Structural: heads must agree; `this`/`Self` are the receiver's type
15118
15811
  // (checked BEFORE the reject set — with a known owner they ARE identity);
@@ -15266,7 +15959,72 @@ function _rustPathIsKnownExternal(index, fileEntry, filePath, receiver, name) {
15266
15959
  `${segments.join('::')}::${name}`, filePath, index.root);
15267
15960
  }
15268
15961
 
15269
- function _rustPatternReceiverType(index, fileEntry, filePath, record) {
15962
+ /** A qualified free function belongs to its exact module, never an impl
15963
+ * whose type or method happens to share either path component. Re-exports
15964
+ * and inline modules abstain until their complete item path is available.
15965
+ */
15966
+ function _rustModuleProducer(index, fileEntry, filePath, call) {
15967
+ if (!call.isPathCall || !call.receiver || call.receiverType || call.localShadow) return null;
15968
+ const parts = call.receiver.split('::');
15969
+ const root = parts[0];
15970
+ const bindings = (fileEntry.importBindings || []).filter(b => (b.alias || b.name) === root);
15971
+ const locals = (index.symbols.get(root) || []).filter(d => d.file === filePath);
15972
+ if (locals.some(d => d.type !== 'module') || bindings.length > 1) return null;
15973
+ const binding = bindings[0];
15974
+ const specifier = binding ? [binding.module, ...parts.slice(1)].join('::') : call.receiver;
15975
+ const terminal = specifier.split('::').at(-1);
15976
+ if (!terminal || ['Self', 'self', 'super', 'crate'].includes(terminal)) return null;
15977
+ const relative = fileEntry.moduleResolved?.[specifier];
15978
+ const destination = relative ? path.resolve(index.root, relative)
15979
+ : resolveRustImport(specifier, filePath, index.root);
15980
+ if (!destination || !index.files.has(destination) || destination === filePath) return null;
15981
+ const base = path.basename(destination, '.rs');
15982
+ if ((base === 'mod' ? path.basename(path.dirname(destination)) : base) !== terminal) return null;
15983
+ const candidates = (index.symbols.get(call.name) || []).filter(d => d.file === destination &&
15984
+ !NON_CALLABLE_TYPES.has(d.type) && !d.className && !d.receiver && !d.namespace);
15985
+ if (candidates.length !== 1 || !candidates[0].returnType) return null;
15986
+ const definition = candidates[0];
15987
+ if ((index.files.get(destination).symbols || []).some(d => d.type === 'module' &&
15988
+ d.startLine < definition.startLine && d.endLine >= definition.endLine)) return null;
15989
+ return { definition, facts: {
15990
+ call: { file: fileEntry.relativePath, line: call.line, start: call.callStart,
15991
+ end: call.callEnd, receiver: call.receiver, name: call.name },
15992
+ binding: binding || null, module: { specifier, file: path.relative(index.root, destination) },
15993
+ declaration: { ...declarationIdentity(definition), returnType: definition.returnType },
15994
+ } };
15995
+ }
15996
+
15997
+ function _rustPatternReceiverType(index, fileEntry, filePath, record, ctx) {
15998
+ if (['Some', 'Ok'].includes(record.receiverPatternVariant) && !record.receiverPatternOwner &&
15999
+ (record.receiverPatternIndex || 0) === 0 &&
16000
+ (record.receiverPatternSourceCallStart != null || record.receiverPatternSourceVariable)) {
16001
+ if (!ctx) {
16002
+ const records = index.getCachedCalls(filePath);
16003
+ ctx = { records, memo: new Map(), visiting: new Set(),
16004
+ getFlowMap: () => _buildReturnTypeFlowMap(index, filePath, records) };
16005
+ }
16006
+ const shadow = (index.symbols.get(record.receiverPatternVariant) || []).some(d => d.file === filePath) ||
16007
+ (fileEntry.importBindings || []).some(b => (b.alias || b.name) === record.receiverPatternVariant || b.name === '*');
16008
+ const sources = record.receiverPatternSourceVariable
16009
+ ? [_lookupReturnTypeFlow(ctx.getFlowMap(), { ...record, receiver: record.receiverPatternSourceVariable,
16010
+ receiverPatternShadow: false })]
16011
+ : ctx.records.filter(source => source.callStart === record.receiverPatternSourceCallStart &&
16012
+ source.callEnd === record.receiverPatternSourceCallEnd).map(source =>
16013
+ _typeOfCallResultFold(index, fileEntry, filePath, source, ctx));
16014
+ const producer = sources.length === 1 && sources[0]?.rustReturnDeclaration;
16015
+ const contract = !shadow && producer && _rustReturnWrapper(index, producer.returnType, producer.file,
16016
+ producer.className, producer, record.receiverPatternProjection || []);
16017
+ if (contract && validateRustWrapperContract(contract) &&
16018
+ contract.kind === (record.receiverPatternVariant === 'Some' ? 'Option' : 'Result')) {
16019
+ return { type: contract.type, fromFile: contract.fromFile,
16020
+ wrapperPattern: { variant: record.receiverPatternVariant, contract,
16021
+ source: { start: record.receiverPatternSourceCallStart, end: record.receiverPatternSourceCallEnd,
16022
+ variable: record.receiverPatternSourceVariable }, shadowDeclarations: [], shadowBindings: [] } };
16023
+ }
16024
+ // A tuple payload cannot be treated as its outer type by the older
16025
+ // whole-payload path below.
16026
+ if (record.receiverPatternProjection?.length) return null;
16027
+ }
15270
16028
  let variants = (index.symbols.get(record.receiverPatternVariant) || [])
15271
16029
  .filter(definition => definition.type === 'variant');
15272
16030
  if (record.receiverPatternOwner) {
@@ -15356,7 +16114,15 @@ const _RUST_ITERATOR_ITEM_CALLBACKS = new Set([
15356
16114
  ]);
15357
16115
 
15358
16116
  function _rustRecordReceiverType(index, fileEntry, filePath, record, ctx) {
15359
- if (record.receiverType && !record.receiverIsChainRoot) {
16117
+ if (record.receiver === 'self') {
16118
+ const enclosing = index.findEnclosingFunction(filePath, record.line, true);
16119
+ if (enclosing?.className) {
16120
+ const origin = _resolveFlowTypeOrigin(index, filePath, enclosing.className);
16121
+ if (origin?.fromFile) return { type: enclosing.className, fromFile: origin.fromFile };
16122
+ }
16123
+ }
16124
+ if (record.receiverType && !record.receiverIsChainRoot &&
16125
+ !record.receiverTypeGuessed && record.receiverTypeSource !== 'guess') {
15360
16126
  const origin = _resolveFlowTypeOrigin(
15361
16127
  index, filePath, record.receiverType, record.receiverTypeQualifier);
15362
16128
  if (origin?.fromFile) {
@@ -15383,9 +16149,13 @@ function _rustRecordReceiverType(index, fileEntry, filePath, record, ctx) {
15383
16149
  }
15384
16150
  if (record.receiverPatternVariant) {
15385
16151
  const pattern = _rustPatternReceiverType(
15386
- index, fileEntry, filePath, record);
16152
+ index, fileEntry, filePath, record, ctx);
15387
16153
  if (pattern) return pattern;
15388
16154
  }
16155
+ if (record.enclosingFunction?.closureParameterNames?.includes(record.receiver)) {
16156
+ const parameter = _rustClosureReceiverType(index, fileEntry, filePath, record, ctx);
16157
+ if (parameter) return parameter;
16158
+ }
15389
16159
  if (record.receiver && !record.receiverIsChainRoot &&
15390
16160
  !record.receiverPatternShadow) {
15391
16161
  const flow = _lookupReturnTypeFlow(ctx.getFlowMap(), record);
@@ -15485,6 +16255,35 @@ const _RUST_ITERATOR_ITEM_PRESERVING = new Set([
15485
16255
  'skip', 'skip_while', 'step_by', 'take', 'take_while',
15486
16256
  ]);
15487
16257
 
16258
+ function _rustIterationReceiverType(index, fileEntry, filePath, call, ctx) {
16259
+ let items = [];
16260
+ let producers = [];
16261
+ if (call.receiverIterationVariable) {
16262
+ const flow = _lookupReturnTypeFlow(ctx.getFlowMap(), {
16263
+ ...call, receiver: call.receiverIterationVariable,
16264
+ });
16265
+ if (flow?.iteratorItemType) {
16266
+ items = [{ type: flow.iteratorItemType, fromFile: flow.iteratorItemFromFile }];
16267
+ producers = [{ ...flow }];
16268
+ }
16269
+ } else {
16270
+ const sources = _chainedProducerRecords(ctx, {
16271
+ receiverCall: call.receiverIterationCall,
16272
+ receiverCallIsMethod: call.receiverIterationCallIsMethod,
16273
+ receiverCallLine: call.receiverIterationCallLine,
16274
+ receiverCallStart: call.receiverIterationCallStart,
16275
+ receiverCallEnd: call.receiverIterationCallEnd,
16276
+ });
16277
+ items = sources.map(source => _rustIteratorOutputItemType(index, fileEntry, filePath, source, ctx));
16278
+ producers = sources.map(source => ({ name: source.name,
16279
+ site: occurrenceIdentity(fileEntry.relativePath, source) }));
16280
+ }
16281
+ if (!items.length || items.some(item => !item?.fromFile) ||
16282
+ new Set(items.map(item => item.type)).size !== 1 ||
16283
+ new Set(items.map(item => item.fromFile)).size !== 1) return null;
16284
+ return { ...items[0], alternatives: items, producers };
16285
+ }
16286
+
15488
16287
  function _rustIteratorOutputItemType(index, fileEntry, filePath, source, ctx, visiting = new Set()) {
15489
16288
  if (!source || visiting.has(source)) return null;
15490
16289
  visiting.add(source);
@@ -15731,6 +16530,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
15731
16530
  externalConcrete: true,
15732
16531
  };
15733
16532
  }
16533
+ const module = language === 'rust' ? _rustModuleProducer(index, fileEntry, filePath, record) : null;
16534
+ if (module) {
16535
+ const producer = module.definition;
16536
+ const parsed = _returnTypeNameNominal(producer.returnType, language);
16537
+ const origin = parsed && _resolveFlowTypeOrigin(index, producer.file, parsed.name, parsed.qualifier);
16538
+ if (!origin) return null;
16539
+ return { type: parsed.name, fromFile: origin.fromFile, moduleProducer: module.facts,
16540
+ rustReturnDeclaration: _rustReturnDeclaration(producer),
16541
+ rustWrapper: _rustReturnWrapper(index, producer.returnType, producer.file, null, producer) };
16542
+ }
15734
16543
  const segs = String(record.receiver).split('::');
15735
16544
  let seg = segs.pop();
15736
16545
  if (seg === 'Self') {
@@ -15872,11 +16681,15 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
15872
16681
  if (record.isMethod) {
15873
16682
  let rt = _goBuiltinChainedReceiverType(
15874
16683
  index, fileEntry, filePath, record);
15875
- if (record.receiverType && !record.receiverIsChainRoot) {
15876
- const origin = nominal
16684
+ if (record.receiverType && !record.receiverIsChainRoot &&
16685
+ !record.receiverTypeGuessed && record.receiverTypeSource !== 'guess') {
16686
+ const origin = language === 'rust'
16687
+ ? _rustFlowReceiverOrigin(index, filePath, record.receiverType, record.receiverTypeQualifier)
16688
+ : nominal
15877
16689
  ? _resolveFlowTypeOrigin(index, filePath, record.receiverType,
15878
16690
  record.receiverTypeQualifier)
15879
16691
  : null;
16692
+ if (language === 'rust' && !origin?.fromFile) return null;
15880
16693
  rt = {
15881
16694
  type: record.receiverType,
15882
16695
  ...(origin?.fromFile && { fromFile: origin.fromFile }),
@@ -16073,7 +16886,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
16073
16886
  if (!parsed) return null;
16074
16887
  const origin = _resolveFlowTypeOrigin(index, chosen.file || filePath, parsed.name, parsed.qualifier);
16075
16888
  if (!origin) return null;
16076
- return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
16889
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }),
16890
+ ...(language === 'rust' && chosen.file === filePath && { rustReturnDeclaration: _rustReturnDeclaration(chosen) }) };
16077
16891
  }
16078
16892
  if (language === 'python' && !consumerAwaited && chosen.isAsync) return null;
16079
16893
  let head = _structuralTypeHead(chosen.returnType, {
@@ -16167,6 +16981,9 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
16167
16981
  const typeTexts = new Set(results.map(r => r.typeText));
16168
16982
  let result = {
16169
16983
  type: results[0].type,
16984
+ ...(results.length === 1 && results[0].moduleProducer && {
16985
+ moduleProducer: results[0].moduleProducer,
16986
+ }),
16170
16987
  ...(typeTexts.size === 1 && results[0].typeText && {
16171
16988
  typeText: results[0].typeText,
16172
16989
  }),
@@ -16317,4 +17134,4 @@ function findCallbackUsages(index, name) {
16317
17134
  return usages;
16318
17135
  }
16319
17136
 
16320
- module.exports = { _unresolvedModuleIsGap, _importReaches, _sameNominalPackageDir, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
17137
+ module.exports = { isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _importReaches, _sameNominalPackageDir, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };