ucn 5.5.0 → 5.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +38 -16
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +41 -27
  6. package/cli/index.js +6 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +106 -31
  9. package/core/analysis.js +266 -127
  10. package/core/ast-analysis.js +12 -3
  11. package/core/brief.js +8 -4
  12. package/core/build-worker.js +2 -1
  13. package/core/cache.js +144 -6
  14. package/core/call-name-cache.js +134 -0
  15. package/core/callable-scope.js +21 -0
  16. package/core/callers.js +5111 -1024
  17. package/core/check.js +22 -5
  18. package/core/class-identity.js +399 -12
  19. package/core/constructor-calls.js +831 -0
  20. package/core/constructor-hierarchy.js +147 -0
  21. package/core/constructor-protocol.js +196 -0
  22. package/core/contract-membership.js +346 -140
  23. package/core/cpp-conversion-constraints.js +123 -0
  24. package/core/cpp-conversion-syntax.js +216 -0
  25. package/core/cpp-conversions.js +414 -0
  26. package/core/cpp-scope.js +245 -44
  27. package/core/deadcode.js +35 -15
  28. package/core/decorator-binding.js +137 -3
  29. package/core/enum-reference-syntax.js +134 -0
  30. package/core/execute.js +17 -1
  31. package/core/external-macros.js +42 -10
  32. package/core/go-packages.js +171 -0
  33. package/core/graph-build.js +2 -0
  34. package/core/graph.js +75 -59
  35. package/core/ignore-rules.js +2 -1
  36. package/core/imports.js +145 -19
  37. package/core/index-ir.js +41 -4
  38. package/core/ir.js +27 -6
  39. package/core/macro-expansion.js +38 -2
  40. package/core/out-of-class-members.js +182 -0
  41. package/core/output/analysis-ext.js +4 -2
  42. package/core/output/analysis.js +89 -20
  43. package/core/output/graph.js +12 -3
  44. package/core/output/lines.js +4 -3
  45. package/core/output/public.js +1 -0
  46. package/core/output/reporting.js +6 -1
  47. package/core/output/shared.js +2 -0
  48. package/core/output/tracing.js +28 -22
  49. package/core/output-budget.js +2 -2
  50. package/core/parallel-build.js +52 -1
  51. package/core/project.js +228 -114
  52. package/core/provenance-constructor-base.js +161 -0
  53. package/core/provenance-facts.js +257 -27
  54. package/core/provenance-overload.js +458 -54
  55. package/core/provenance-type.js +487 -0
  56. package/core/provenance.js +234 -15
  57. package/core/public-command.js +17 -2
  58. package/core/query-bodies.js +159 -0
  59. package/core/reflection.js +255 -2
  60. package/core/rename-token-syntax.js +111 -0
  61. package/core/reporting.js +13 -7
  62. package/core/rust-macro-expansion.js +1202 -157
  63. package/core/rust-modules.js +21 -3
  64. package/core/rust-names.js +607 -0
  65. package/core/search.js +70 -17
  66. package/core/shared.js +98 -1
  67. package/core/source-memo.js +201 -0
  68. package/core/text-usage-syntax.js +38 -0
  69. package/core/tracing.js +7 -1
  70. package/core/type-denotation.js +157 -46
  71. package/core/type-references.js +615 -246
  72. package/core/value-references.js +2777 -0
  73. package/core/verify.js +1097 -281
  74. package/languages/adapter.js +28 -4
  75. package/languages/c-family.js +1118 -164
  76. package/languages/constructor-bases.js +105 -0
  77. package/languages/cpp-platform.js +26 -0
  78. package/languages/cpp-site-syntax.js +83 -0
  79. package/languages/csharp-scope.js +439 -0
  80. package/languages/csharp.js +428 -68
  81. package/languages/go.js +385 -207
  82. package/languages/html.js +4 -2
  83. package/languages/index.js +397 -2
  84. package/languages/java-platform.js +226 -0
  85. package/languages/java-unicode.js +269 -0
  86. package/languages/java.js +461 -117
  87. package/languages/javascript.js +418 -66
  88. package/languages/lexical-scope.js +190 -23
  89. package/languages/python.js +404 -54
  90. package/languages/rust.js +548 -89
  91. package/languages/utils.js +67 -8
  92. package/mcp/server.js +8 -2
  93. package/package.json +2 -2
@@ -45,6 +45,7 @@ const fs = require('fs');
45
45
  const path = require('path');
46
46
  const { langTraits } = require('../languages');
47
47
  const { findCargoRoot, findGoModule } = require('./imports');
48
+ const { projectFact, projectHas, fileSymbolFact } = require('./provenance-overload');
48
49
 
49
50
  const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum', 'record', 'union']);
50
51
 
@@ -232,15 +233,20 @@ function projectNamespaces(index, language) {
232
233
  });
233
234
  }
234
235
 
236
+ /** Does the project declare namespace/package `name`? A witness records
237
+ * the answer, never every file the inventory reads (fix #418). */
238
+ function projectNamespaceDeclared(index, language, name) {
239
+ return projectHas(index, `namespaces:${language}`, name, live => projectNamespaces(live, language));
240
+ }
241
+
235
242
  /** Is some leading dotted prefix of `qualified` (the full path excluded) a
236
243
  * namespace/package the project declares? `org.junit.Assert` is project
237
244
  * code when `org.junit` is a project package; nested types keep their
238
245
  * package prefix (`a.b.Outer.Inner`). */
239
246
  function hasProjectNamespacePrefix(index, language, qualified) {
240
- const names = projectNamespaces(index, language);
241
247
  const segments = String(qualified).split('.');
242
248
  for (let i = segments.length - 1; i > 0; i--) {
243
- if (names.has(segments.slice(0, i).join('.'))) return true;
249
+ if (projectNamespaceDeclared(index, language, segments.slice(0, i).join('.'))) return true;
244
250
  }
245
251
  return false;
246
252
  }
@@ -363,7 +369,10 @@ function externalTypeDenotation(index, filePath, name, qualifier, line, options
363
369
  // `unscoped`: the site's spelling of the name is unknown (the parser kept
364
370
  // only the last segment), so package/namespace scoping cannot apply.
365
371
  const unscoped = options.unscoped === true;
366
- const key = `${filePath}\x00${name}\x00${qualifier || ''}\x00${scoped ?? ''}\x00${unscoped ? 1 : 0}`;
372
+ // Java/C# scoping reads the site's package or namespace (fix #433): a
373
+ // C# file with several namespace blocks decides a name per block.
374
+ const siteScope = !qualifier && !unscoped ? scopingSiteKey(index, filePath, line) : '';
375
+ const key = `${filePath}\x00${name}\x00${qualifier || ''}\x00${scoped ?? ''}\x00${unscoped ? 1 : 0}\x00${siteScope}`;
367
376
  return memoized(index, key, () => {
368
377
  const fileEntry = index.files.get(filePath);
369
378
  if (!fileEntry) return null;
@@ -465,7 +474,7 @@ function scopedTypeDenotation(index, fileEntry, filePath, name, line, scoping) {
465
474
  }
466
475
  if (implicitHas) return { original: name, via: `${implicit.module}.${name}` };
467
476
  const external = onDemand.filter(module => !hasProjectNamespacePrefix(index, language, `${module}.${name}`) &&
468
- !projectNamespaces(index, language).has(module));
477
+ !projectNamespaceDeclared(index, language, module));
469
478
  if (external.length === 0) return null;
470
479
  return { original: name, via: `${external[0]}.*`,
471
480
  ...(!external.every(module => standardRooted(language, module)) && { certain: false }) };
@@ -485,10 +494,26 @@ function scopedTypeDenotation(index, fileEntry, filePath, name, line, scoping) {
485
494
  // `using static T` also brings T's nested types (already abstained above).
486
495
  const usings = new Set([...scope.namespaces, ...scope.statics]);
487
496
  if (projectTypes.some(definition => usings.has(definition.namespace))) return null;
488
- const external = [...usings].filter(module => !csharpDeclaredNames(index).namespaces.has(module));
497
+ const external = [...usings].filter(module =>
498
+ !projectHas(index, 'cs-declared-namespaces', module, live => csharpDeclaredNames(live).namespaces));
489
499
  return { original: name, via: external.length > 0 ? `${external.sort()[0]}.${name}` : name };
490
500
  }
491
501
 
502
+ /** What `scopedTypeDenotation` reads of the site: its package/namespace
503
+ * (`siteNamespaceOf`), constant for a file whose types share one namespace;
504
+ * '' for a language without type-name scoping. */
505
+ function scopingSiteKey(index, filePath, line) {
506
+ const fileEntry = index.files.get(filePath);
507
+ if (!fileEntry || !langTraits(fileEntry.language)?.typeNameScoping) return '';
508
+ const namespaces = memoized(index, `\x01site-ns\x00${filePath}`, () => new Set(
509
+ fileSymbolFact(index, filePath, `file-type-namespaces\0${filePath}`, (symbols, live) => symbols
510
+ ? memoized(live, `\x01site-ns-facts\x00${filePath}`, () => symbols
511
+ .filter(symbol => TYPE_KINDS.has(symbol.type)).map(symbol => symbol.namespace || '')) : [])));
512
+ if (namespaces.size <= 1) return '\x01';
513
+ const namespace = siteNamespaceOf(fileEntry, line);
514
+ return namespace === undefined ? '\x02' : `\x03${namespace}`;
515
+ }
516
+
492
517
  /** Could a project type named `name` be the type `fqn` names? Only a type
493
518
  * in a named package/namespace that prefixes it (nested types included). */
494
519
  function fqnMayName(index, language, name, fqn) {
@@ -551,10 +576,12 @@ function csharpResolveUsingName(index, written, enclosing) {
551
576
  const text = String(written || '').replace(/\s+/g, '').replace(/^global::/, '');
552
577
  if (!enclosing || !text) return text;
553
578
  const first = text.split('.')[0].replace(/<.*$/s, '');
554
- const { namespaces, types } = csharpDeclaredNames(index);
555
579
  for (let parts = String(enclosing).split('.'); parts.length > 0; parts = parts.slice(0, -1)) {
556
580
  const candidate = `${parts.join('.')}.${first}`;
557
- if (namespaces.has(candidate) || types.has(candidate)) return `${parts.join('.')}.${text}`;
581
+ if (projectHas(index, 'cs-declared-namespaces', candidate, live => csharpDeclaredNames(live).namespaces) ||
582
+ projectHas(index, 'cs-declared-types', candidate, live => csharpDeclaredNames(live).types)) {
583
+ return `${parts.join('.')}.${text}`;
584
+ }
558
585
  }
559
586
  return text;
560
587
  }
@@ -584,17 +611,36 @@ function csharpUsingTarget(text) {
584
611
  return String(text || '').replace(/<.*$/s, '');
585
612
  }
586
613
 
587
- function csharpAddDirective(scope, index, { module, names, static: isStatic, namespace }) {
588
- const target = csharpUsingTarget(csharpResolveUsingName(index, module, namespace));
614
+ function csharpAddDirective(scope, index, { module, names, static: isStatic, namespace, lexicalNamespace = namespace, rooted = false }) {
615
+ const syntax = csharpResolveUsingName(index, module,
616
+ rooted || String(module || '').startsWith('global::') ? '' : namespace);
617
+ const target = csharpUsingTarget(syntax);
589
618
  if (!target) return;
590
619
  const alias = (names || []).find(name => name && name !== '*');
591
620
  if (alias) {
621
+ const at = lexicalNamespace || '';
622
+ if (!scope.aliasDefinitions.has(alias)) scope.aliasDefinitions.set(alias, []);
623
+ scope.aliasDefinitions.get(alias).push({ namespace: at, syntax,
624
+ absolute: rooted || String(module || '').startsWith('global::') });
625
+ const previous = scope.aliasScopes?.get(alias);
626
+ // Project-file imports are added after source directives; that order
627
+ // cannot override a nearer namespace's alias.
628
+ if (scope.scoped && previous != null && previous.length > at.length) return;
629
+ if (scope.aliases.has(alias) && previous === at && scope.aliases.get(alias) !== target) {
630
+ scope.ambiguousAliases.add(alias);
631
+ } else if (previous !== at) scope.ambiguousAliases.delete(alias);
592
632
  scope.aliases.set(alias, target);
633
+ scope.aliasSyntax.set(alias, syntax);
593
634
  if (!scope.aliasScopes) scope.aliasScopes = new Map();
594
- scope.aliasScopes.set(alias, namespace || '');
635
+ scope.aliasScopes.set(alias, at);
636
+ } else {
637
+ const values = isStatic ? scope.statics : scope.namespaces;
638
+ const origins = isStatic ? scope.staticScopes : scope.namespaceScopes;
639
+ values.add(target);
640
+ if (isStatic) scope.staticSyntax.set(target, syntax);
641
+ if (!origins.has(target)) origins.set(target, new Set());
642
+ origins.get(target).add(lexicalNamespace || '');
595
643
  }
596
- else if (isStatic) scope.statics.add(target);
597
- else scope.namespaces.add(target);
598
644
  }
599
645
 
600
646
  /**
@@ -602,29 +648,53 @@ function csharpAddDirective(scope, index, { module, names, static: isStatic, nam
602
648
  * directives (resolved from the namespace each is written in), `global
603
649
  * using` directives of every source file, and the <Using> items of the
604
650
  * project files above it (`csharpProjectFileUsings`).
605
- * Returns { namespaces: Set, statics: Set (type names), aliases: Map
606
- * (alias -> target), unreadable } - unreadable when a project file could not
651
+ * An optional namespace restricts local directives to that lexical chain;
652
+ * origins and original alias syntax support positive identity proofs.
653
+ * Returns namespaces/statics Sets, aliases and origin Maps, and unreadable.
654
+ * unreadable is true when a project file could not
607
655
  * be read (its items are unknown).
608
656
  */
609
- function csharpUsings(index, filePath) {
610
- return memoized(index, `\x01cs-usings\x00${filePath}`, () => {
611
- const scope = { namespaces: new Set(), statics: new Set(), aliases: new Map(), unreadable: false };
612
- const globals = memoized(index, '\x01cs-global-usings', () => {
657
+ function csharpUsings(index, filePath, namespace = null, siteNode = null, syntax = null) {
658
+ const scopes = syntax?.scopes || (siteNode ? require('../languages/csharp-scope').usingScopesOf(siteNode) : null);
659
+ const scopeKey = scopes?.map(scope => `${scope.startIndex}:${scope.endIndex}`).join(',') ?? '*';
660
+ return memoized(index, `\x01cs-usings\x00${filePath}\x00${namespace ?? '*'}\x00${scopeKey}`, () => {
661
+ const scope = { namespaces: new Set(), statics: new Set(), aliases: new Map(),
662
+ ambiguousAliases: new Set(), aliasSyntax: new Map(), aliasDefinitions: new Map(), scoped: namespace != null,
663
+ namespaceScopes: new Map(), staticScopes: new Map(), staticSyntax: new Map(), unreadable: false };
664
+ const globals = projectFact(index, 'cs-global-usings', live => memoized(live, '\x01cs-global-usings', () => {
613
665
  const out = [];
614
- for (const [, entry] of index.files) {
666
+ for (const [, entry] of live.files) {
615
667
  if (entry.language !== 'csharp') continue;
616
668
  for (const detail of entry.importDetails || []) {
617
669
  if (detail.global && detail.module) out.push(detail);
618
670
  }
619
671
  }
620
672
  return out;
621
- });
673
+ }));
622
674
  for (const detail of globals) csharpAddDirective(scope, index, detail);
623
675
  const fileEntry = index.files.get(filePath);
624
- for (const detail of fileEntry?.importDetails || []) {
625
- if (detail.type === 'using' && detail.module && !detail.global) {
626
- csharpAddDirective(scope, index, detail);
627
- }
676
+ let local = fileEntry?.importDetails || [];
677
+ if (syntax) local = syntax.directives;
678
+ else if (scopes) {
679
+ const content = index._readFile(filePath);
680
+ const language = require('../languages');
681
+ const parser = language.getParser(fileEntry.language);
682
+ const adapter = language.getLanguageAdapter(fileEntry.language);
683
+ const tree = (index.isProvenanceFactIndex && siteNode.tree) ||
684
+ (require('./query-bodies').isLocalNameTree(siteNode.tree) && siteNode.tree) ||
685
+ index._getParsedTree(filePath, content, fileEntry.language) ||
686
+ adapter.queryTree(content, parser, fileEntry);
687
+ local = adapter.queryUsingDirectives(tree);
688
+ }
689
+ for (const detail of local) {
690
+ if (detail.type !== 'using' || !detail.module || detail.global) continue;
691
+ if (scopes) {
692
+ const owner = detail.usingScopes.at(-1);
693
+ if (owner && !scopes.some(current => current.startIndex === owner.startIndex &&
694
+ current.endIndex === owner.endIndex)) continue;
695
+ csharpAddDirective(scope, index, { ...detail, lexicalNamespace: owner?.namespace || '' });
696
+ } else if (namespace == null || !detail.namespace || namespace === detail.namespace ||
697
+ namespace.startsWith(detail.namespace + '.')) csharpAddDirective(scope, index, detail);
628
698
  }
629
699
  for (const item of csharpProjectFileUsings(index, filePath)) {
630
700
  if (item === null) { scope.unreadable = true; continue; }
@@ -643,26 +713,41 @@ function csharpUsings(index, filePath) {
643
713
  */
644
714
  function csharpExpandAliasTargets(scope) {
645
715
  const scopes = scope.aliasScopes;
646
- if (!scopes || scope.aliases.size < 2) return;
716
+ if (!scopes) return;
647
717
  const encloses = (outer, inner) => outer !== inner && (outer === '' || inner.startsWith(`${outer}.`));
648
- for (let pass = 0; pass < 4; pass++) {
649
- let changed = false;
650
- for (const [alias, target] of scope.aliases) {
651
- const [head, ...rest] = String(target).split('.');
652
- if (head === alias || !scope.aliases.has(head)) continue;
653
- if (!encloses(scopes.get(head) ?? '', scopes.get(alias) ?? '')) continue;
654
- scope.aliases.set(alias, [scope.aliases.get(head), ...rest].join('.'));
655
- changed = true;
718
+ const expand = (definition, seen = new Set()) => {
719
+ if (definition.absolute) return definition.syntax;
720
+ if (seen.has(definition) || seen.size >= 16) return null;
721
+ const [head, ...rest] = definition.syntax.split('.');
722
+ const outer = (scope.aliasDefinitions.get(head) || []).filter(other =>
723
+ encloses(other.namespace, definition.namespace));
724
+ if (!outer.length) return definition.syntax;
725
+ const nearest = Math.max(...outer.map(other => other.namespace.length));
726
+ const next = new Set(seen); next.add(definition);
727
+ const targets = new Set(outer.filter(other => other.namespace.length === nearest)
728
+ .map(other => expand(other, next)));
729
+ if (targets.has(null) || targets.size !== 1) return null;
730
+ return [[...targets][0], ...rest].join('.');
731
+ };
732
+ for (const [alias, at] of scopes) {
733
+ const definitions = (scope.aliasDefinitions.get(alias) || []).filter(definition => definition.namespace === at);
734
+ const targets = new Set(definitions.map(definition => expand(definition)));
735
+ if (targets.has(null) || targets.size !== 1) {
736
+ scope.ambiguousAliases.add(alias);
737
+ continue;
656
738
  }
657
- if (!changed) break;
739
+ const syntax = [...targets][0];
740
+ scope.aliasSyntax.set(alias, syntax);
741
+ scope.aliases.set(alias, csharpUsingTarget(syntax));
658
742
  }
659
743
  }
660
744
 
661
745
  /** The directory whose `.git` marks the repository holding `dir`, or null. */
662
746
  function gitTopOf(index, dir) {
663
747
  return memoized(index, `\x01git-top\x00${dir}`, () => {
748
+ const sourceFs = index.isProvenanceFactIndex ? index.provenanceFs : fs;
664
749
  for (let current = dir; ; current = path.dirname(current)) {
665
- try { if (fs.existsSync(path.join(current, '.git'))) return current; } catch { /* keep walking */ }
750
+ try { if (sourceFs.existsSync(path.join(current, '.git'))) return current; } catch { /* keep walking */ }
666
751
  if (current === path.dirname(current)) return null;
667
752
  }
668
753
  });
@@ -679,6 +764,7 @@ function gitTopOf(index, dir) {
679
764
  * null marks a file that could not be read.
680
765
  */
681
766
  function csharpProjectFileUsings(index, filePath) {
767
+ const sourceFs = index.isProvenanceFactIndex ? index.provenanceFs : fs;
682
768
  const dirs = [];
683
769
  const root = path.resolve(index.root);
684
770
  let dir = path.dirname(filePath);
@@ -697,12 +783,12 @@ function csharpProjectFileUsings(index, filePath) {
697
783
  }
698
784
  return dirs.flatMap(current => memoized(index, `\x01cs-proj-usings\x00${current}`, () => {
699
785
  let names;
700
- try { names = fs.readdirSync(current); } catch { return []; }
786
+ try { names = sourceFs.readdirSync(current); } catch { return []; }
701
787
  const out = [];
702
788
  for (const file of names.sort()) {
703
789
  if (!/\.csproj$|^Directory\.Build\.(props|targets)$/.test(file)) continue;
704
790
  let text;
705
- try { text = fs.readFileSync(path.join(current, file), 'utf-8'); } catch { out.push(null); continue; }
791
+ try { text = sourceFs.readFileSync(path.join(current, file), 'utf-8'); } catch { out.push(null); continue; }
706
792
  for (const match of text.matchAll(/<Using\b([^>]*)>/g)) {
707
793
  const include = /\bInclude\s*=\s*"([^"]+)"/.exec(match[1]);
708
794
  if (!include) continue;
@@ -774,11 +860,14 @@ function rustScopedLine(index, filePath, line, name) {
774
860
  if (line == null) return null;
775
861
  const fileEntry = index.files.get(filePath);
776
862
  if (fileEntry?.language !== 'rust') return null;
777
- const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
778
- (TYPE_KINDS.has(definition.type) || definition.type === 'type'));
779
- const imports = (fileEntry.importBindings || []).some(binding =>
780
- binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
781
- return declares || imports ? line : null;
863
+ // Per file and name (fix #405: every `T::new()` site asked again).
864
+ const scoped = memoized(index, `scoped\x00${filePath}\x00${name}`, () => {
865
+ const declares = (index.symbols.get(name) || []).some(definition => definition.file === filePath &&
866
+ (TYPE_KINDS.has(definition.type) || definition.type === 'type'));
867
+ return declares || (fileEntry.importBindings || []).some(binding =>
868
+ binding.name !== '*' && (binding.alias || binding.name) === name && binding.module);
869
+ });
870
+ return scoped ? line : null;
782
871
  }
783
872
 
784
873
  /** Bindings visible at `line`: those of the innermost enclosing scope that
@@ -946,6 +1035,30 @@ function projectTypeNamed(index, name) {
946
1035
  return (index.symbols.get(name) || []).some(definition => TYPE_KINDS.has(definition.type));
947
1036
  }
948
1037
 
1038
+ /**
1039
+ * The type a call record writes and what it denotes in its file. A record
1040
+ * never changes once its file's calls are read, so the answer is kept per
1041
+ * record object while the index lives (fix #405b: orientation refines every
1042
+ * definition of a name over the same records, and each refinement asked
1043
+ * again per record).
1044
+ */
1045
+ function siteDenotation(index, filePath, fileEntry, call) {
1046
+ const memos = memoOf(index);
1047
+ let memo = memos.get('\x01site');
1048
+ if (!memo) memos.set('\x01site', memo = new WeakMap());
1049
+ let site = memo.get(call);
1050
+ if (site === undefined) {
1051
+ const written = siteWrittenType(index, fileEntry, call);
1052
+ site = {
1053
+ written,
1054
+ denotation: written ? externalTypeDenotation(index, filePath, written.name, written.qualifier,
1055
+ call.line, { unscoped: written.unscoped === true }) : null,
1056
+ };
1057
+ memo.set(call, site);
1058
+ }
1059
+ return site;
1060
+ }
1061
+
949
1062
  /**
950
1063
  * Verdict for a call site against candidate target definitions (the pinned
951
1064
  * targets on the caller side, every same-name callable on the callee side).
@@ -961,10 +1074,7 @@ function externalTypeGate(index, filePath, fileEntry, call, targetsOrThunk) {
961
1074
  // every project type with the method set satisfies; the existing
962
1075
  // package-qualified physics routes it (possible dispatch).
963
1076
  if (fileEntry.language === 'go') return null;
964
- const written = siteWrittenType(index, fileEntry, call);
965
- if (!written) return null;
966
- const denotation = externalTypeDenotation(index, filePath, written.name, written.qualifier, call.line,
967
- { unscoped: written.unscoped === true });
1077
+ const { written, denotation } = siteDenotation(index, filePath, fileEntry, call);
968
1078
  if (!denotation) return null;
969
1079
  const { original, via } = denotation;
970
1080
  // Only a real collision needs the gate: without a project type of that
@@ -1143,6 +1253,7 @@ function stripExternalReceiverType(call, via) {
1143
1253
  }
1144
1254
 
1145
1255
  module.exports = {
1256
+ memoizedDerivation: memoized,
1146
1257
  externalTypeDenotation,
1147
1258
  externalReceiverVerdict,
1148
1259
  externalProducerVia,