ucn 5.3.4 → 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.
@@ -8,12 +8,15 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { createProvenance, validateConfirmation } = require('./provenance');
12
+
11
13
  // Resolution types ordered from most to least confident
12
14
  const RESOLUTION = {
13
15
  EXACT_BINDING: 'exact-binding',
14
16
  SAME_CLASS: 'same-class',
15
17
  RECEIVER_HINT: 'receiver-hint',
16
18
  SCOPE_MATCH: 'scope-match',
19
+ SINGLE_OWNER: 'single-owner',
17
20
  POSSIBLE_DISPATCH: 'possible-dispatch',
18
21
  NAME_ONLY: 'name-only',
19
22
  METHOD_AMBIGUOUS: 'method-ambiguous',
@@ -27,6 +30,7 @@ const SCORES = {
27
30
  [RESOLUTION.SAME_CLASS]: 0.92,
28
31
  [RESOLUTION.RECEIVER_HINT]: 0.80,
29
32
  [RESOLUTION.SCOPE_MATCH]: 0.65,
33
+ [RESOLUTION.SINGLE_OWNER]: 0.35,
30
34
  [RESOLUTION.POSSIBLE_DISPATCH]: 0.50,
31
35
  [RESOLUTION.NAME_ONLY]: 0.40,
32
36
  [RESOLUTION.METHOD_AMBIGUOUS]: 0.35,
@@ -46,6 +50,7 @@ const RESOLUTION_TIER = {
46
50
  // scope-match is only assigned with import/receiver/callback evidence
47
51
  // (see scoreEdge below) — that satisfies the contract's evidence clause.
48
52
  [RESOLUTION.SCOPE_MATCH]: TIER.CONFIRMED,
53
+ [RESOLUTION.SINGLE_OWNER]: TIER.UNVERIFIED,
49
54
  // Nominal dispatch tiering: a call that CAN reach the target through
50
55
  // virtual dispatch (interface/supertype-typed receiver) or whose untyped
51
56
  // receiver faces multiple same-name owners is evidence a call happens —
@@ -61,7 +66,7 @@ function tierForResolution(resolution) {
61
66
  return RESOLUTION_TIER[resolution] || TIER.UNVERIFIED;
62
67
  }
63
68
 
64
- function scored(resolution, reasons) {
69
+ function scored(resolution, reasons, evidence) {
65
70
  const evidenceScore = SCORES[resolution];
66
71
  return {
67
72
  confidence: evidenceScore,
@@ -69,6 +74,7 @@ function scored(resolution, reasons) {
69
74
  scoreKind: 'ordinal-evidence-not-probability',
70
75
  resolution,
71
76
  evidence: reasons,
77
+ provenance: createProvenance(evidence || {}, resolution),
72
78
  };
73
79
  }
74
80
 
@@ -95,38 +101,44 @@ function scoreEdge(evidence) {
95
101
  // (without this, a known mismatch would score receiver-hint 0.80).
96
102
  if (evidence.typeMismatch) {
97
103
  reasons.push('receiver type mismatch');
98
- return scored(RESOLUTION.UNCERTAIN, reasons);
104
+ return scored(RESOLUTION.UNCERTAIN, reasons, evidence);
99
105
  }
100
106
 
101
107
  // Nominal dispatch tiering (contract surface only — callers.js sets these
102
108
  // flags exclusively under collectAccount, so legacy paths never see them).
103
109
  if (evidence.possibleDispatch) {
104
110
  reasons.push('interface/supertype dispatch');
105
- return scored(RESOLUTION.POSSIBLE_DISPATCH, reasons);
111
+ return scored(RESOLUTION.POSSIBLE_DISPATCH, reasons, evidence);
106
112
  }
107
113
  if (evidence.methodAmbiguous) {
108
114
  reasons.push('untyped receiver, multiple same-name definitions');
109
- return scored(RESOLUTION.METHOD_AMBIGUOUS, reasons);
115
+ return scored(RESOLUTION.METHOD_AMBIGUOUS, reasons, evidence);
110
116
  }
111
117
 
112
118
  // Exact binding match (highest confidence)
113
119
  if (evidence.hasBindingId) {
114
120
  reasons.push('binding-id match');
115
121
  if (evidence.hasImportEvidence) reasons.push('import-verified');
116
- return scored(RESOLUTION.EXACT_BINDING, reasons);
122
+ return scored(RESOLUTION.EXACT_BINDING, reasons, evidence);
117
123
  }
118
124
 
119
125
  // Same-class resolution (self/this/super/cls)
120
126
  if (evidence.resolvedBySameClass) {
121
127
  reasons.push('same-class method');
122
128
  if (evidence.hasInheritanceChain) reasons.push('via inheritance');
123
- return scored(RESOLUTION.SAME_CLASS, reasons);
129
+ return scored(RESOLUTION.SAME_CLASS, reasons, evidence);
124
130
  }
125
131
 
126
132
  // Receiver hint narrowed to specific type
127
133
  if (evidence.resolvedByReceiverHint || evidence.hasReceiverType) {
128
134
  reasons.push(evidence.hasReceiverType ? 'parser receiver-type' : 'local type inference');
129
- return scored(RESOLUTION.RECEIVER_HINT, reasons);
135
+ return scored(RESOLUTION.RECEIVER_HINT, reasons, evidence);
136
+ }
137
+
138
+ if (evidence.hasSingleOwnerEvidence && !evidence.typeQualifiedReceiver &&
139
+ !evidence.moduleOwnedPath) {
140
+ reasons.push('single project owner, receiver identity unresolved');
141
+ return scored(RESOLUTION.SINGLE_OWNER, reasons, evidence);
130
142
  }
131
143
 
132
144
  // Function reference (callback / passed-as-argument). Argument position is
@@ -137,31 +149,30 @@ function scoreEdge(evidence) {
137
149
  reasons.push('function reference');
138
150
  if (evidence.hasImportEvidence || evidence.hasSamePackageEvidence) {
139
151
  reasons.push(evidence.hasImportEvidence ? 'import-supported' : 'same package/module');
140
- return scored(RESOLUTION.SCOPE_MATCH, reasons);
152
+ return scored(RESOLUTION.SCOPE_MATCH, reasons, evidence);
141
153
  }
142
154
  reasons.push('no import evidence');
143
- return scored(RESOLUTION.NAME_ONLY, reasons);
155
+ return scored(RESOLUTION.NAME_ONLY, reasons, evidence);
144
156
  }
145
157
 
146
158
  // Scope/import-supported match
147
159
  if (evidence.hasImportEvidence || evidence.hasReceiverEvidence ||
148
- evidence.hasSamePackageEvidence || evidence.hasSingleOwnerEvidence) {
160
+ evidence.hasSamePackageEvidence) {
149
161
  if (evidence.hasImportEvidence) reasons.push('import-supported');
150
162
  if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
151
163
  if (evidence.hasSamePackageEvidence) reasons.push('same package/module');
152
- if (evidence.hasSingleOwnerEvidence) reasons.push('single project method owner');
153
- return scored(RESOLUTION.SCOPE_MATCH, reasons);
164
+ return scored(RESOLUTION.SCOPE_MATCH, reasons, evidence);
154
165
  }
155
166
 
156
167
  // Uncertain
157
168
  if (evidence.isUncertain) {
158
169
  reasons.push('ambiguous resolution');
159
- return scored(RESOLUTION.UNCERTAIN, reasons);
170
+ return scored(RESOLUTION.UNCERTAIN, reasons, evidence);
160
171
  }
161
172
 
162
173
  // Name-only match (no additional evidence)
163
174
  reasons.push('name match only');
164
- return scored(RESOLUTION.NAME_ONLY, reasons);
175
+ return scored(RESOLUTION.NAME_ONLY, reasons, evidence);
165
176
  }
166
177
 
167
178
  /**
@@ -192,4 +203,5 @@ module.exports = {
192
203
  tierForResolution,
193
204
  scoreEdge,
194
205
  filterByConfidence,
206
+ validateConfirmation,
195
207
  };
package/core/imports.js CHANGED
@@ -446,18 +446,32 @@ function rustModuleOwnFile(dir, fromFile) {
446
446
  * @param {string[]} segments - Path segments to resolve
447
447
  * @returns {string|null}
448
448
  */
449
+ function rustPathHasExactCase(base, file) {
450
+ // Rust names remain case-sensitive on case-insensitive filesystems.
451
+ // Check directory entries, rather than realpath, so legitimate symlinked
452
+ // modules keep their declared spelling and remain resolvable.
453
+ let current = base;
454
+ try {
455
+ for (const part of path.relative(base, file).split(path.sep)) {
456
+ if (!fs.readdirSync(current).includes(part)) return false;
457
+ current = path.join(current, part);
458
+ }
459
+ return true;
460
+ } catch { return false; }
461
+ }
462
+
449
463
  function resolveRustModulePath(dir, segments) {
450
464
  // Try progressively shorter paths (items at the end may be types, not modules)
451
465
  for (let len = segments.length; len >= 1; len--) {
452
466
  const modPath = path.join(dir, ...segments.slice(0, len));
453
467
  // Try <path>.rs
454
468
  const rsFile = modPath + '.rs';
455
- if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
469
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile() && rustPathHasExactCase(dir, rsFile)) {
456
470
  return rsFile;
457
471
  }
458
472
  // Try <path>/mod.rs
459
473
  const modFile = path.join(modPath, 'mod.rs');
460
- if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
474
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile() && rustPathHasExactCase(dir, modFile)) {
461
475
  return modFile;
462
476
  }
463
477
  }
@@ -600,11 +614,11 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
600
614
  if (!importPath.includes('::')) {
601
615
  // For mod declarations: <dir>/<name>.rs or <dir>/<name>/mod.rs
602
616
  const rsFile = path.join(fromDir, importPath + '.rs');
603
- if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile()) {
617
+ if (fs.existsSync(rsFile) && fs.statSync(rsFile).isFile() && rustPathHasExactCase(fromDir, rsFile)) {
604
618
  return rsFile;
605
619
  }
606
620
  const modFile = path.join(fromDir, importPath, 'mod.rs');
607
- if (fs.existsSync(modFile) && fs.statSync(modFile).isFile()) {
621
+ if (fs.existsSync(modFile) && fs.statSync(modFile).isFile() && rustPathHasExactCase(fromDir, modFile)) {
608
622
  return modFile;
609
623
  }
610
624
  }
package/core/index-ir.js CHANGED
@@ -81,7 +81,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
81
81
  'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
82
82
  'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
83
83
  'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
84
- 'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
84
+ 'aliasOf', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
85
85
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
86
86
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
87
87
  'registryMember', 'registryContainer', 'namespace',
package/core/ir.js CHANGED
@@ -63,7 +63,7 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
63
63
  'docstring', 'returnedFunctionResult', 'isFunctionVariable', 'paramTypes',
64
64
  'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent',
65
65
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType',
66
- 'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
66
+ 'aliasOf', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
67
67
  'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
68
68
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
69
69
  'registryMember', 'registryContainer', 'isConstructor',
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { langTraits } = require('../../languages');
7
7
  const { dynamicImportsNote, formatGitLine, unverifiedReasonLabel } = require('./shared');
8
+ const { provenanceReplacer } = require('./provenance');
8
9
 
9
10
  /**
10
11
  * One short sentence (~80 chars) of a docstring, suitable for inline display
@@ -40,7 +41,7 @@ function reachabilityDisplay(items, hasEntrypoints, label) {
40
41
  }
41
42
 
42
43
  // Display order for resolution labels in evidence aggregates (most → least confident)
43
- const RESOLUTION_ORDER = ['exact-binding', 'same-class', 'receiver-hint', 'scope-match', 'name-only', 'uncertain'];
44
+ const RESOLUTION_ORDER = ['exact-binding', 'same-class', 'receiver-hint', 'scope-match', 'single-owner', 'name-only', 'uncertain'];
44
45
 
45
46
  /**
46
47
  * One aggregate evidence line per tier section, replacing per-edge confidence
@@ -51,8 +52,9 @@ function formatEvidenceLine(items) {
51
52
  if (!items || items.length === 0) return null;
52
53
  const counts = new Map();
53
54
  for (const it of items) {
54
- if (!it.resolution) continue;
55
- counts.set(it.resolution, (counts.get(it.resolution) || 0) + 1);
55
+ const rule = it.provenance?.rule || it.resolution;
56
+ if (!rule) continue;
57
+ counts.set(rule, (counts.get(rule) || 0) + 1);
56
58
  }
57
59
  if (counts.size === 0) return null;
58
60
  if (counts.size === 1) {
@@ -266,6 +268,8 @@ function formatContextJson(context) {
266
268
  ...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
267
269
  ...(c.scoreKind && { scoreKind: c.scoreKind }),
268
270
  ...(c.resolution && { resolution: c.resolution }),
271
+ ...(c.provenance && { provenance: c.provenance }),
272
+ ...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
269
273
  ...(c.tier && { tier: c.tier })
270
274
  })),
271
275
  unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
@@ -281,6 +285,8 @@ function formatContextJson(context) {
281
285
  ...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
282
286
  ...(c.scoreKind && { scoreKind: c.scoreKind }),
283
287
  ...(c.resolution && { resolution: c.resolution }),
288
+ ...(c.provenance && { provenance: c.provenance }),
289
+ ...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
284
290
  ...(c.reason && { reason: c.reason }),
285
291
  ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
286
292
  ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
@@ -295,7 +301,7 @@ function formatContextJson(context) {
295
301
  }),
296
302
  ...(context.warnings && { warnings: context.warnings })
297
303
  }
298
- });
304
+ }, provenanceReplacer);
299
305
  }
300
306
 
301
307
  // Standard function/method context
@@ -321,6 +327,8 @@ function formatContextJson(context) {
321
327
  ...(c.calledAs && { calledAs: c.calledAs }),
322
328
  ...(c.isFunctionReference && { functionReference: true }),
323
329
  ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
330
+ ...(c.provenance && { provenance: c.provenance }),
331
+ ...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
324
332
  ...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
325
333
  ...(c.scoreKind && { scoreKind: c.scoreKind }),
326
334
  ...(c.tier && { tier: c.tier }),
@@ -335,6 +343,8 @@ function formatContextJson(context) {
335
343
  ...(c.calledAs && { calledAs: c.calledAs }),
336
344
  ...(c.isFunctionReference && { functionReference: true }),
337
345
  ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
346
+ ...(c.provenance && { provenance: c.provenance }),
347
+ ...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
338
348
  ...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
339
349
  ...(c.scoreKind && { scoreKind: c.scoreKind }),
340
350
  tier: 'unverified',
@@ -358,6 +368,8 @@ function formatContextJson(context) {
358
368
  params: c.params, // FULL params
359
369
  weight: c.weight || 'normal', // Dependency weight: core, setup, utility
360
370
  ...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
371
+ ...(c.provenance && { provenance: c.provenance }),
372
+ ...(c.siteProvenance && { siteProvenance: c.siteProvenance }),
361
373
  ...(c.evidenceScore != null && { evidenceScore: c.evidenceScore }),
362
374
  ...(c.scoreKind && { scoreKind: c.scoreKind }),
363
375
  ...(c.tier && { tier: c.tier }),
@@ -370,7 +382,7 @@ function formatContextJson(context) {
370
382
  unverifiedCallees: context.unverifiedCallees || [],
371
383
  ...(context.warnings && { warnings: context.warnings })
372
384
  }
373
- });
385
+ }, provenanceReplacer);
374
386
  }
375
387
 
376
388
  /**
@@ -456,7 +468,7 @@ function formatContext(ctx, options = {}) {
456
468
  for (const u of typeUnverified) {
457
469
  if (shown >= cap) break;
458
470
  const callerName = u.callerName ? ` [${u.callerName}]` : '';
459
- const reason = u.reason ? ` (${u.reason})` : '';
471
+ const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
460
472
  const expr = u.content ? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
461
473
  lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}${reason}`);
462
474
  expandable.push({
@@ -943,7 +955,7 @@ function formatImpactJson(impact) {
943
955
  if (!impact) {
944
956
  return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
945
957
  }
946
- return JSON.stringify(impact, null, 2);
958
+ return JSON.stringify(impact, provenanceReplacer, 2);
947
959
  }
948
960
 
949
961
  /** Format about command output - text. The "tell me everything" output for AI agents. */
@@ -1215,7 +1227,7 @@ function formatAboutJson(about) {
1215
1227
  if (!about) {
1216
1228
  return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
1217
1229
  }
1218
- return JSON.stringify(about, null, 2);
1230
+ return JSON.stringify(about, provenanceReplacer, 2);
1219
1231
  }
1220
1232
 
1221
1233
  module.exports = {
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ // Full witnesses stay on engine results for validation and oracle reports.
4
+ // Public answers carry the facts an agent needs to locate and interpret a site,
5
+ // rather than repeating declaration/member inventories for every occurrence.
6
+ function compactProvenance(provenance) {
7
+ if (!provenance) return provenance;
8
+ const facts = provenance.facts || {};
9
+ const origin = facts.receiverOrigin;
10
+ return {
11
+ rule: provenance.rule,
12
+ ...(provenance.rules?.length > 1 && { rules: provenance.rules }),
13
+ validation: provenance.validation,
14
+ ...(provenance.diagnostic && provenance.validation !== 'unsupported' &&
15
+ { diagnostic: provenance.diagnostic }),
16
+ ...(facts.receiverTypeSource && { receiverSource: facts.receiverTypeSource }),
17
+ ...(Number.isInteger(origin?.line) && { originLine: origin.line }),
18
+ };
19
+ }
20
+
21
+ function compactExclusions(excluded) {
22
+ if (!Array.isArray(excluded?.evidence)) return excluded;
23
+ const { evidence, ...summary } = excluded;
24
+ const groups = new Map();
25
+ for (const site of evidence) {
26
+ const p = site.provenance || {};
27
+ const row = { rule: p.rule || 'unattributed', validation: p.validation || 'unreported',
28
+ ...(p.diagnostic && p.validation !== 'unsupported' && { diagnostic: p.diagnostic }) };
29
+ const key = JSON.stringify(row);
30
+ if (!groups.has(key)) groups.set(key, { ...row, count: 0 });
31
+ groups.get(key).count++;
32
+ }
33
+ return { ...summary, evidenceSummary: [...groups.values()] };
34
+ }
35
+
36
+ function provenanceReplacer(key, value) {
37
+ if (key === 'provenance') return compactProvenance(value);
38
+ return key === 'excluded' ? compactExclusions(value) : value;
39
+ }
40
+
41
+ module.exports = { compactProvenance, compactExclusions, provenanceReplacer };
@@ -12,6 +12,7 @@ const { COMMAND_CONTRACTS } = require('../command-contracts');
12
12
  const { COMMAND_TRUST_MATRIX } = require('../trust-matrix');
13
13
  const { toCliName, toMcpName, formatSurfaceMessage } = require('../registry');
14
14
  const { LINES_COMMANDS, formatPublicLines, formatPublicRaw } = require('./lines');
15
+ const { provenanceReplacer } = require('./provenance');
15
16
 
16
17
  const legacy = {
17
18
  ...require('./analysis'),
@@ -44,7 +45,7 @@ function canonicalJsonValue(value) {
44
45
  if (!value || typeof value !== 'object') return value;
45
46
  const canonical = {};
46
47
  for (const key of Object.keys(value).sort()) {
47
- canonical[key] = canonicalJsonValue(value[key]);
48
+ canonical[key] = canonicalJsonValue(provenanceReplacer(key, value[key]));
48
49
  }
49
50
  return canonical;
50
51
  }
@@ -342,6 +342,9 @@ function formatGitLine(git) {
342
342
  */
343
343
  function unverifiedReasonLabel(entry) {
344
344
  if (!entry || !entry.reason) return '';
345
+ if (entry.reason === 'provenance-incomplete' && entry.provenance?.diagnostic) {
346
+ return `${entry.reason}: ${entry.provenance.diagnostic}`;
347
+ }
345
348
  if (entry.reason === 'possible-dispatch' && entry.externalContract) {
346
349
  // External contract (fix #210): the candidate set is open — any
347
350
  // external subtype of the contract — so no implementation count.