ucn 5.3.4 → 5.3.6
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/.claude/skills/ucn/SKILL.md +22 -0
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +5 -1
- package/core/callers.js +989 -124
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +8 -3
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +185 -77
- package/languages/type-evidence.js +63 -0
- package/package.json +2 -2
package/core/confidence.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
@@ -12,9 +12,14 @@ function createImportBindings(imports) {
|
|
|
12
12
|
return imports.flatMap(item => (item.names || [])
|
|
13
13
|
.filter(name => name && name !== '*' && name !== '_' && name !== '.')
|
|
14
14
|
.map(name => {
|
|
15
|
-
|
|
15
|
+
// A rename may be recorded under its original name (Python
|
|
16
|
+
// `from m import a as b` lists 'a') or under its local alias
|
|
17
|
+
// (Rust `use m::a as b` lists 'b', fix #357); both yield the
|
|
18
|
+
// binding {name: original, alias: local}.
|
|
19
|
+
const rename = (item.renames || []).find(candidate =>
|
|
20
|
+
candidate.original === name || candidate.local === name);
|
|
16
21
|
return {
|
|
17
|
-
name,
|
|
22
|
+
name: rename ? rename.original : name,
|
|
18
23
|
module: item.module,
|
|
19
24
|
...(item.type && { kind: item.type }),
|
|
20
25
|
...(item.line != null && { line: item.line }),
|
|
@@ -81,7 +86,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
|
|
|
81
86
|
'returnedFunctionResult', 'isFunctionVariable', 'paramTypes', 'isAsync',
|
|
82
87
|
'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'extends', 'implements', 'indent', 'isNested',
|
|
83
88
|
'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType',
|
|
84
|
-
'aliasOf', 'aliasMembers', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
89
|
+
'aliasOf', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
|
|
85
90
|
'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
|
|
86
91
|
'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'bodyScopedName',
|
|
87
92
|
'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',
|
package/core/output/analysis.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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 };
|
package/core/output/public.js
CHANGED
|
@@ -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
|
}
|
package/core/output/shared.js
CHANGED
|
@@ -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.
|