ucn 5.0.5 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,7 +53,7 @@ not an accuracy grade.
53
53
  For caller-bearing `show`, `impact`, `trace`, `tests`, and `check` views:
54
54
 
55
55
  - `CONFIRMED` has binding, receiver, import, or ownership evidence for the pinned target.
56
- - `UNVERIFIED` is a possible target with insufficient identity evidence. Review it before a breaking change.
56
+ - `UNVERIFIED` is a possible target with insufficient identity evidence. Review it before a breaking change. When same-name definitions cause the ambiguity, `show` lists their stable handles once above the sites so the competing targets are explicit.
57
57
  - `ACCOUNT` partitions observed literal-name lines into confirmed, unverified, non-call, excluded, and unresolved buckets. In mixed-language repositories it also counts occurrences in unsupported-language source files.
58
58
  - `CONTRACT` states the scope and completeness of that observed-text partition. When unsupported-language files contain the name, it says so explicitly and the partition-complete claim is limited to supported languages.
59
59
  - `WARNING` identifies unreadable, unparsed, or partially indexed files, and lists unsupported-language occurrence sites (file:line plus the line text) so nothing grep would show is hidden. `usages` and `tests` carry the same disclosure as a note.
@@ -41,16 +41,17 @@ Even when all eight hold, `account.contract.semanticComplete` remains false. Use
41
41
 
42
42
  Semantic recall can be complete while the result is still impractical: a true edge may be present only in a large unverified set. Release evaluation therefore measures both placement and the amount of review an agent must perform.
43
43
 
44
- - `trueEdgeUnverifiedRate` is the share of semantic oracle edges found only in the unverified tier.
44
+ - `trueEdgeUnverifiedRate` is the share of statically exact oracle edges found only in the unverified tier. Runtime-polymorphic, compiler-dependent template, and oracle-unresolved references remain in the all-oracle view but never inflate the exact denominator.
45
45
  - `zeroActionableUnverifiedTargetRate` is the share of reviewed targets with no actionable ambiguity.
46
46
  - `actionableUnverifiedCandidatesP50`, `actionableUnverifiedCandidatesP95`, and `actionableUnverifiedCandidatesMax` measure actionable candidate-set size per target.
47
47
  - `unverifiedReviewItemsPerOracleEdge` measures effective review work relative to the oracle workload. Actionable false candidates count individually; each named runtime-dispatch family counts once.
48
48
  - `rawFalseUnverifiedPerOracleEdge` preserves the ungrouped false-candidate amplification for auditability.
49
+ - `runtimeDependentOracleEdges`, `compilerDependentOracleEdges`, and `oracleAbstentionEdges` disclose why all-oracle coverage is broader than exact target identity.
49
50
  - `unverifiedReasons` groups candidates by the engine reason that kept them out of the confirmed tier.
50
51
 
51
52
  Configuration-gated candidates remain visible but are not labeled false when the compiler/LSP oracle did not score them. The raw JSON rollup is the source of truth for these fields; the Markdown report is generated from the same data.
52
53
 
53
- The publish-blocking portable-AST ceilings are: at most 10% of exact true edges left unverified, at most 20% within any sufficiently sampled symbol kind, at least 80% of targets with zero actionable ambiguity, actionable-candidate p95 at most five, and at most 0.10 effective review items per oracle edge. The semantic gate separately requires 100% in-scope recall, at least 98% confirmed-tier precision, full conservation, and 100% public-command proof recall. Passing these sampled floors is release evidence, not runtime-completeness proof.
54
+ The publish-blocking portable-AST ceilings are: at most 10% of exact true edges left unverified, at most 20% within any sufficiently sampled symbol kind, at least 80% of targets with zero actionable ambiguity, actionable-candidate p95 at most five, and at most 0.10 effective review items per oracle edge. The semantic gate separately requires 100% in-scope recall, at least 98% confirmed-tier precision, full conservation, and 100% public-command proof recall. Three class-aware floors gate at zero: unaccounted oracle edges of the exact, runtime-dispatch, and oracle-unresolved classes (caller and callee arms; compile-time-dispatch attributions stay report-only because their non-exact band varies with the compiler oracle's environment), runtime-dispatch oracle edges excluded with reason (possible-dispatch routing is demote-only), and deferred oracle-unresolved edges that exact definition lookup pins to the target re-enter the gate-bearing universe. Passing these sampled floors is release evidence, not runtime-completeness proof.
54
55
 
55
56
  The performance gate runs each pinned repository in three independent processes with a fixed worker shape. Publish and PR gates require both CPU and wall-throughput floors on the median run, pin the expected file/LOC workload, and use the worst observed build/full-board peak RSS so memory failures cannot be averaged away. Exploratory one-process runs keep wall throughput diagnostic. Scoped reports are separate from the full release artifact; the dated rollup is release-qualified only when every required row came from a full release invocation. The composite release gate runs this stage first so verbose compiler-oracle output and report consumers cannot perturb its samples.
56
57
 
package/README.md CHANGED
@@ -638,7 +638,9 @@ answers. It's guidance over the same engine, not a second implementation.
638
638
  and external consumers can be invisible. UCN reports these blind spots
639
639
  (`repo --sections=health --deep`) rather than pretending they don't exist.
640
640
  - Interface, trait, template, overload, and untyped-receiver dispatch may stay
641
- in the UNVERIFIED tier with a reason instead of being guessed.
641
+ in the UNVERIFIED tier with a reason instead of being guessed. When
642
+ same-name definitions compete, `show` lists their stable handles once so
643
+ agents can see exactly what needs disambiguation.
642
644
  - C/C++ analysis doesn't run the preprocessor or compiler; build-specific
643
645
  branches, advanced templates, and macro expansion can remain unresolved. C#
644
646
  analysis doesn't run Roslyn; source generators and external assembly
package/core/analysis.js CHANGED
@@ -12,7 +12,8 @@ const path = require('path');
12
12
  const { execFileSync } = require('child_process');
13
13
  const { parse } = require('./parser');
14
14
  const { detectLanguage, langTraits } = require('../languages');
15
- const { NON_CALLABLE_TYPES, addTestExclusions, countTextBlindspots, codeUnitCompare } = require('./shared');
15
+ const { NON_CALLABLE_TYPES, addTestExclusions, countTextBlindspots,
16
+ codeUnitCompare, formatSymbolHandle } = require('./shared');
16
17
  const { isTestFile } = require('./discovery');
17
18
  const { computeReachability, symbolKey } = require('./entrypoints');
18
19
  const { getLanguageAdapter } = require('../languages');
@@ -23,6 +24,62 @@ const { projectComputedDispatch } = require('./ast-analysis');
23
24
  // passed to one of these (the common pattern in mocha/jest/vitest).
24
25
  const _JS_TEST_FRAMEWORK_CALLS = new Set(['describe', 'it', 'test', 'spec', 'context', 'suite']);
25
26
 
27
+ /**
28
+ * Give agents the exact indexed definitions behind a same-name ambiguity.
29
+ * Candidate handles live once on the context result rather than being copied
30
+ * onto every unverified site (a single broad name can have thousands).
31
+ */
32
+ function buildAmbiguityCandidates(index, name, selected, unverified) {
33
+ const relevant = (unverified || []).filter(site =>
34
+ site.reason === 'method-ambiguous' || site.reason === 'ambiguous-binding');
35
+ if (relevant.length === 0) return null;
36
+ const byHandle = new Map();
37
+ for (const definition of (index.symbols.get(name) || [])) {
38
+ if (!definition.file || !definition.startLine) continue;
39
+ const handle = formatSymbolHandle(definition);
40
+ if (handle && !byHandle.has(handle)) byHandle.set(handle, definition);
41
+ }
42
+ const definitions = [...byHandle.values()]
43
+ .sort((a, b) => {
44
+ const aSelected = a.bindingId === selected.bindingId ||
45
+ (a.file === selected.file && a.startLine === selected.startLine);
46
+ const bSelected = b.bindingId === selected.bindingId ||
47
+ (b.file === selected.file && b.startLine === selected.startLine);
48
+ if (aSelected !== bSelected) return aSelected ? -1 : 1;
49
+ const fileCmp = codeUnitCompare(a.relativePath || a.file, b.relativePath || b.file);
50
+ if (fileCmp !== 0) return fileCmp;
51
+ if (a.startLine !== b.startLine) return a.startLine - b.startLine;
52
+ return codeUnitCompare(a.type, b.type);
53
+ });
54
+ if (definitions.length < 2) return null;
55
+ const limit = 12;
56
+ const items = definitions.slice(0, limit).map(definition => ({
57
+ handle: formatSymbolHandle(definition),
58
+ type: definition.type,
59
+ ...(definition.className && { owner: definition.className }),
60
+ ...(!definition.className && definition.receiver && {
61
+ owner: definition.receiver.replace(/^\*/, ''),
62
+ }),
63
+ ...(!definition.className && !definition.receiver && definition.assignedReceiver && {
64
+ owner: definition.assignedReceiver,
65
+ memberAssignment: true,
66
+ }),
67
+ selected: definition.bindingId === selected.bindingId ||
68
+ (definition.file === selected.file && definition.startLine === selected.startLine),
69
+ }));
70
+ // reduce, not Math.max(...spread): a broad name on a monorepo can carry
71
+ // enough unverified sites to overflow the argument list.
72
+ const dispatchOwners = relevant.reduce(
73
+ (max, site) => Math.max(max, Number(site.dispatchCandidates) || 0), 0);
74
+ return {
75
+ name,
76
+ totalDefinitions: definitions.length,
77
+ ...(dispatchOwners > 0 && { dispatchOwners }),
78
+ items,
79
+ truncated: definitions.length > items.length,
80
+ };
81
+ }
82
+
26
83
  /**
27
84
  * Tag each call site with `inTestCase` based on its enclosing function's
28
85
  * entry-point classification. Uses each language's `getEntryPointKind`
@@ -338,6 +395,8 @@ function context(index, name, options = {}) {
338
395
 
339
396
  typeCallers = [...typeCallers].sort(byFileLine);
340
397
  typeUnverified = [...typeUnverified].sort(byFileLine);
398
+ const typeAmbiguityCandidates = buildAmbiguityCandidates(
399
+ index, name, def, typeUnverified);
341
400
 
342
401
  const callerTotal = typeCallers.length;
343
402
  if (options.maxCallers && options.maxCallers > 0) {
@@ -366,6 +425,9 @@ function context(index, name, options = {}) {
366
425
  // Also include places where the type is used in function parameters/returns
367
426
  callers: typeCallers,
368
427
  unverifiedCallers: typeUnverified,
428
+ ...(typeAmbiguityCandidates && {
429
+ ambiguityCandidates: typeAmbiguityCandidates,
430
+ }),
369
431
  meta: { account: typeAccount, callerTotal }
370
432
  };
371
433
 
@@ -485,6 +547,8 @@ function context(index, name, options = {}) {
485
547
 
486
548
  const callerHistogram = buildHistogram(callers);
487
549
  const calleeHistogram = buildHistogram(callees);
550
+ const ambiguityCandidates = buildAmbiguityCandidates(
551
+ index, name, def, unverifiedCallers);
488
552
  const callerTotal = callers.length;
489
553
  const calleeTotal = callees.length;
490
554
  if (options.maxCallers && options.maxCallers > 0) {
@@ -512,6 +576,7 @@ function context(index, name, options = {}) {
512
576
  returnType: def.returnType,
513
577
  callers,
514
578
  unverifiedCallers,
579
+ ...(ambiguityCandidates && { ambiguityCandidates }),
515
580
  callees,
516
581
  unverifiedCallees: rawCallees.unverifiedCallees || [],
517
582
  callerHistogram,
package/core/bridge.js CHANGED
@@ -835,7 +835,8 @@ function collectProjectRouterMounts(index) {
835
835
  const targetEntry = index.files.get(targetFile);
836
836
  if (targetEntry) {
837
837
  const exportedReceivers = (targetEntry.exportDetails || [])
838
- .filter(exp => exp.type === 'module.exports' || exp.isDefault ||
838
+ .filter(exp => (exp.type === 'module.exports' && exp.defaultLike) ||
839
+ exp.isDefault ||
839
840
  exp.kind === 'default' || exp.type === 'export-default' ||
840
841
  exp.name === ref.name)
841
842
  .map(exp => exp.localName || exp.name).filter(Boolean);
package/core/cache.js CHANGED
@@ -599,7 +599,25 @@ function clearAllCaches() {
599
599
  // v166: C++ nested aliases persist their lexical owner ranges, and `auto`
600
600
  // return functions persist a unanimously inferred local concrete type. v165
601
601
  // was used during prerelease development before both fields were complete.
602
- const CACHE_FORMAT_VERSION = 167;
602
+ // v173: JS/TS fluent methods persist AST-proven `this` return identity, and
603
+ // nested const/arrow callables persist their enclosing lexical owner range.
604
+ // v172: immutable JS/TS class-member aliases materialize their local and
605
+ // exported callable identities, preserving declared return types through
606
+ // factory aliases and chained receivers.
607
+ // v171: import bindings persist their syntactic kind (named/default/namespace)
608
+ // so an exported ESM namespace object can carry exact member ownership through
609
+ // a downstream named or default import.
610
+ // v170: statically-owned CommonJS property assignments persist their local
611
+ // callable identity, allowing exact module ownership to exclude a different
612
+ // same-name export without treating all CJS surfaces as opaque.
613
+ // v169: CommonJS export details distinguish the value assigned directly to
614
+ // module.exports (`defaultLike`) from property exports; this prevents a
615
+ // namespace require from becoming a confirmed call to every exported member.
616
+ // v168: JS/TS one-hop member-assignment defs persist assignedReceiver — the
617
+ // object they patch (`console.log = fn` → 'console') — so the builtin-global
618
+ // exclusion can see cross-file that a project def rebinds the global's
619
+ // member (fix #286a); impl-kind symbols leave the bindings table (#286b).
620
+ const CACHE_FORMAT_VERSION = 173;
603
621
 
604
622
  /**
605
623
  * Save index to cache file
@@ -640,7 +658,9 @@ function saveCache(index, cachePath) {
640
658
  // Strip redundant fields from symbols and file entries to reduce cache size.
641
659
  // v6: All paths stored as relative paths (saves ~60% on large codebases).
642
660
  // symbol.file = path.join(root, symbol.relativePath) — reconstructable
643
- // symbol.bindingId = relativePath:type:startLine — reconstructable
661
+ // Default symbol.bindingId = relativePath:type:startLine — reconstructable.
662
+ // Preserve non-default IDs: synthetic declarations can share a source line
663
+ // and need their explicit identity to survive a cache round-trip.
644
664
  // fileEntry.path = Map key — redundant
645
665
  // fileEntry.relativePath = now the Map key — redundant
646
666
  const root = index.root;
@@ -648,6 +668,12 @@ function saveCache(index, cachePath) {
648
668
  for (const [name, defs] of index.symbols) {
649
669
  const stripped = defs.map(s => {
650
670
  const { file, bindingId, ...rest } = s;
671
+ const defaultBindingId = s.relativePath && s.type && s.startLine
672
+ ? `${s.relativePath}:${s.type}:${s.startLine}`
673
+ : null;
674
+ if (bindingId && bindingId !== defaultBindingId) {
675
+ rest.bindingId = bindingId;
676
+ }
651
677
  return rest;
652
678
  });
653
679
  strippedSymbols.push([name, stripped]);
@@ -897,12 +923,15 @@ function loadCache(index, cachePath) {
897
923
  const fileEntry = index.files.get(s.file);
898
924
  if (fileEntry) {
899
925
  fileEntry.symbols.push(s);
900
- fileEntry.bindings.push({
901
- id: s.bindingId,
902
- name: s.name,
903
- type: s.type,
904
- startLine: s.startLine
905
- });
926
+ if (!s.memberAssigned && !s.bodyScopedName && !s.exportedAlias &&
927
+ s.type !== 'impl') {
928
+ fileEntry.bindings.push({
929
+ id: s.bindingId,
930
+ name: s.name,
931
+ type: s.type,
932
+ startLine: s.startLine
933
+ });
934
+ }
906
935
  }
907
936
  }
908
937
  }