ucn 5.5.3 → 5.6.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.
- package/.claude/skills/ucn/SKILL.md +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Validate changed calls against the project declarations they can reach. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { getCachedCalls } = require('./callers');
|
|
5
|
+
const { verifyCallSites } = require('./verify');
|
|
6
|
+
const { langTraits } = require('../languages');
|
|
7
|
+
const { positivelyFits } = require('./call-fit');
|
|
8
|
+
|
|
9
|
+
/** One reader and result cache for a check, including all changed symbols. */
|
|
10
|
+
function outgoingChecker(index, judge, addedLinesByFile) {
|
|
11
|
+
const files = new Map();
|
|
12
|
+
const checked = new Map();
|
|
13
|
+
const read = file => {
|
|
14
|
+
if (!files.has(file)) files.set(file, {
|
|
15
|
+
calls: getCachedCalls(index, file) || [], lines: index._getFileLines(file),
|
|
16
|
+
});
|
|
17
|
+
return files.get(file);
|
|
18
|
+
};
|
|
19
|
+
return (definition, change, added) => {
|
|
20
|
+
const mismatches = [];
|
|
21
|
+
const unsettled = [];
|
|
22
|
+
if (!definition) return { mismatches, unsettled };
|
|
23
|
+
const data = read(definition.file);
|
|
24
|
+
const changedRanges = !added ? change?.changedCalls : null;
|
|
25
|
+
const lines = addedLinesByFile.get(definition.relativePath) || new Set();
|
|
26
|
+
const records = data.calls.filter(call => !call.isFunctionReference &&
|
|
27
|
+
call.line >= definition.startLine && call.line <= definition.endLine &&
|
|
28
|
+
(changedRanges ? changedRanges.some(range =>
|
|
29
|
+
range.start <= (call.callStart ?? call.callSite?.start) &&
|
|
30
|
+
range.end >= (call.callEnd ?? call.callSite?.end ?? call.callStart ?? call.callSite?.start))
|
|
31
|
+
: added || lines.has(call.line)));
|
|
32
|
+
if (records.length === 0) return { mismatches, unsettled };
|
|
33
|
+
const byStart = new Map(records.map(call => [call.callStart ?? call.callSite?.start, call]));
|
|
34
|
+
const callees = index.findCallees(definition, {
|
|
35
|
+
collectAccount: true, includeMethods: true, siteStarts: new Set(byStart.keys()),
|
|
36
|
+
});
|
|
37
|
+
const relative = definition.relativePath;
|
|
38
|
+
const positiveFamilies = new Set();
|
|
39
|
+
const possibleFamilies = new Set();
|
|
40
|
+
const issues = [];
|
|
41
|
+
const familyOf = (callee, site) => {
|
|
42
|
+
// The class's public constructor overloads are the call
|
|
43
|
+
// interface; an initializer edge also projects their body.
|
|
44
|
+
const owner = callee.initializerOf || (callee.type === 'class' ? callee.name : null);
|
|
45
|
+
return JSON.stringify([site.start, callee.file, owner || callee.name,
|
|
46
|
+
owner ? null : callee.className, callee.namespace, callee.enclosingType]);
|
|
47
|
+
};
|
|
48
|
+
const uncertain = (site, name, reason) => unsettled.push({ file: relative, line: site.line, column: site.column, name, reason });
|
|
49
|
+
for (const callee of callees) {
|
|
50
|
+
if (callee.functionReference) continue;
|
|
51
|
+
for (const site of callee.siteProvenance || []) {
|
|
52
|
+
const record = byStart.get(site.start);
|
|
53
|
+
if (!record) continue;
|
|
54
|
+
const family = familyOf(callee, site);
|
|
55
|
+
const key = JSON.stringify([definition.file, site.start, callee.file, callee.startLine,
|
|
56
|
+
callee.sourceSpan?.start, callee.sourceSpan?.end, callee.name, callee.className,
|
|
57
|
+
callee.type, callee.paramsStructured]);
|
|
58
|
+
let result = checked.get(key);
|
|
59
|
+
if (!result) {
|
|
60
|
+
result = verifyCallSites(index, callee, [{
|
|
61
|
+
...record, file: definition.file, relativePath: relative,
|
|
62
|
+
content: data.lines[site.line - 1] || '', calledAs: record.name,
|
|
63
|
+
provenance: site.provenance,
|
|
64
|
+
callerFile: definition.file, callerStartLine: definition.startLine,
|
|
65
|
+
}]);
|
|
66
|
+
checked.set(key, result);
|
|
67
|
+
}
|
|
68
|
+
for (const mismatch of result.mismatchDetails || []) {
|
|
69
|
+
const counts = String(mismatch.expected || '').match(/\d+/g);
|
|
70
|
+
const suppressed = judge.suppressed(mismatch, {
|
|
71
|
+
kind: 'arity', name: callee.name,
|
|
72
|
+
most: counts ? Number(counts[counts.length - 1]) : undefined,
|
|
73
|
+
span: mismatch.span, problem: mismatch.problem, indirect: mismatch.indirect,
|
|
74
|
+
});
|
|
75
|
+
// JavaScript deliberately permits omitted and extra
|
|
76
|
+
// arguments. A declared count is not runtime proof.
|
|
77
|
+
const flexible = langTraits(index.files.get(definition.file)?.language)?.lenientCallArity === true;
|
|
78
|
+
if ((callee.decorators || []).length) uncertain(site, callee.name, 'decorated-call-interface');
|
|
79
|
+
else if (flexible) uncertain(site, callee.name, 'call-arity-not-enforced');
|
|
80
|
+
else issues.push({ family, site, callee, mismatch: {
|
|
81
|
+
...mismatch, column: site.column, target: callee.name, ...(suppressed && { unchecked: true }),
|
|
82
|
+
} });
|
|
83
|
+
}
|
|
84
|
+
if (result.valid > 0 || result.uncertain > 0) possibleFamilies.add(family);
|
|
85
|
+
for (const row of result.uncertainDetails || []) issues.push({ family, site, callee, reason: row.reason });
|
|
86
|
+
if (result.valid > 0) {
|
|
87
|
+
const preserved = changedRanges?.some(range => range.stableInvocation &&
|
|
88
|
+
range.start <= site.start && range.end >= (record.callEnd ?? record.callSite?.end ?? site.start));
|
|
89
|
+
if (positivelyFits(index, callee, definition, record, result, preserved)) positiveFamilies.add(family);
|
|
90
|
+
else issues.push({ family, site, callee, reason: 'argument-compatibility-unverified' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Overload declarations describe one call interface. A positively
|
|
95
|
+
// fitting alternative settles it; a merely possible alternative
|
|
96
|
+
// prevents a different signature's arity from claiming a break.
|
|
97
|
+
for (const issue of issues) {
|
|
98
|
+
if (positiveFamilies.has(issue.family)) continue;
|
|
99
|
+
if (issue.mismatch && !possibleFamilies.has(issue.family)) mismatches.push(issue.mismatch);
|
|
100
|
+
else uncertain(issue.site, issue.callee.name, issue.reason || 'overload-compatibility-unverified');
|
|
101
|
+
}
|
|
102
|
+
for (const entry of [...(callees.unverifiedCallees || []), ...(callees.initializerReviews || [])]) {
|
|
103
|
+
// External packages are outside this fit check. A candidate
|
|
104
|
+
// project declaration, however, cannot silently become READY.
|
|
105
|
+
if (!(entry.ownerCount > 0 || entry.initializerOf || index.symbols.has(entry.name))) continue;
|
|
106
|
+
for (const site of entry.siteProvenance || []) {
|
|
107
|
+
if (byStart.has(site.start)) uncertain(site, entry.name, entry.reason || 'outgoing-target-unresolved');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const unique = rows => [...new Map(rows.map(row => [JSON.stringify(row), row])).values()]
|
|
111
|
+
.sort((a, b) => a.line - b.line || (a.column || 0) - (b.column || 0) || ((a.name || a.target || '') < (b.name || b.target || '') ? -1 : (a.name || a.target || '') > (b.name || b.target || '') ? 1 : 0));
|
|
112
|
+
return { mismatches: unique(mismatches), unsettled: unique(unsettled) };
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { outgoingChecker };
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { dynamicImportsNote, formatLineRanges, unverifiedReasonLabel, advisoryLine } = require('./shared');
|
|
6
|
-
const {
|
|
6
|
+
const { paramFact } = require('../registry');
|
|
7
|
+
const { formatAccountLines, formatSharedAccountLines, formatCalleeAccountLine, unverifiedCalleeLines, initializerReviewLines } = require('./analysis');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Format related command output - text
|
|
@@ -200,6 +201,8 @@ function formatDiffImpact(result, options = {}) {
|
|
|
200
201
|
|
|
201
202
|
const lines = [];
|
|
202
203
|
const MAX_CALLERS_PER_FN = options.all ? Infinity : 30;
|
|
204
|
+
const unverifiedLimit = options.all ? Infinity
|
|
205
|
+
: Number.isInteger(options.top) && options.top > 0 ? options.top : paramFact('top', 'unverifiedRows');
|
|
203
206
|
|
|
204
207
|
lines.push(`Diff Impact Analysis (vs ${result.base})`);
|
|
205
208
|
lines.push('═'.repeat(60));
|
|
@@ -262,7 +265,7 @@ function formatDiffImpact(result, options = {}) {
|
|
|
262
265
|
// Unverified tier: visible one-liners with reasons (never silently dropped)
|
|
263
266
|
const unverified = fn.unverifiedCallers || [];
|
|
264
267
|
if (unverified.length > 0) {
|
|
265
|
-
const cap =
|
|
268
|
+
const cap = unverifiedLimit;
|
|
266
269
|
lines.push(` Unverified call sites (${unverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
267
270
|
for (const u of unverified.slice(0, cap)) {
|
|
268
271
|
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
@@ -275,11 +278,15 @@ function formatDiffImpact(result, options = {}) {
|
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
|
|
278
|
-
// Conservation contract lines, indented inside the function block
|
|
279
|
-
for
|
|
281
|
+
// Conservation contract lines, indented inside the function block.
|
|
282
|
+
// What holds for every symbol of the repository is said once,
|
|
283
|
+
// after the last function.
|
|
284
|
+
for (const al of formatAccountLines(fn.account, { shared: false })) {
|
|
280
285
|
lines.push(` ${al}`);
|
|
281
286
|
}
|
|
282
287
|
}
|
|
288
|
+
const withAccount = result.functions.find(fn => fn.account);
|
|
289
|
+
if (withAccount) lines.push(...formatSharedAccountLines(withAccount.account));
|
|
283
290
|
}
|
|
284
291
|
|
|
285
292
|
// New functions
|
|
@@ -300,11 +307,12 @@ function formatDiffImpact(result, options = {}) {
|
|
|
300
307
|
const remaining = fn.remainingCallSites || [];
|
|
301
308
|
if (remaining.length > 0) {
|
|
302
309
|
lines.push(` ⚠ still called from ${remaining.length} site(s) — name-level matches:`);
|
|
303
|
-
|
|
310
|
+
const cap = unverifiedLimit;
|
|
311
|
+
for (const site of remaining.slice(0, cap)) {
|
|
304
312
|
lines.push(` ${site.relativePath}:${site.line} ${site.content}`);
|
|
305
313
|
}
|
|
306
|
-
if (remaining.length >
|
|
307
|
-
lines.push(` ... and ${remaining.length -
|
|
314
|
+
if (remaining.length > cap) {
|
|
315
|
+
lines.push(` ... and ${remaining.length - cap} more`);
|
|
308
316
|
}
|
|
309
317
|
}
|
|
310
318
|
}
|
|
@@ -316,14 +324,16 @@ function formatDiffImpact(result, options = {}) {
|
|
|
316
324
|
for (const symbol of result[key]) {
|
|
317
325
|
lines.push(` ${symbol.type} ${symbol.name} — ${symbol.relativePath}:${symbol.startLine}`);
|
|
318
326
|
if (symbol.impact) {
|
|
319
|
-
lines.push(require('./analysis').formatImpact(symbol.impact, { compact: true
|
|
327
|
+
lines.push(require('./analysis').formatImpact(symbol.impact, { compact: true,
|
|
328
|
+
all: options.all, top: options.top, allHint: options.allHint,
|
|
329
|
+
evidenceBands: options.evidenceBands }));
|
|
320
330
|
} else {
|
|
321
331
|
const refs = symbol.remainingReferences || [];
|
|
322
332
|
lines.push(` Remaining name references: ${refs.length} unverified (deleted target)`);
|
|
323
|
-
for (const site of refs.slice(0,
|
|
333
|
+
for (const site of refs.slice(0, unverifiedLimit)) {
|
|
324
334
|
lines.push(` ${site.file}:${site.line}: ${site.expression}`);
|
|
325
335
|
}
|
|
326
|
-
if (refs.length >
|
|
336
|
+
if (refs.length > unverifiedLimit) lines.push(` (+${refs.length - unverifiedLimit} more)`);
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
}
|
package/core/output/analysis.js
CHANGED
|
@@ -5,7 +5,11 @@ 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 { paramFact } = require('../registry');
|
|
8
9
|
const { provenanceReplacer } = require('./provenance');
|
|
10
|
+
const { matchLabelCounts, reasonList } = require('../source-universe');
|
|
11
|
+
const { uncertaintyClassOf, partitionUnverified, runtimeFamilyKey } = require('../review-items');
|
|
12
|
+
const { reasonSummaryLine } = require('../evidence-bands');
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* One short sentence (~80 chars) of a docstring, suitable for inline display
|
|
@@ -93,12 +97,43 @@ function formatAmbiguityCandidates(lines, ambiguity) {
|
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The account lines that hold for every symbol of the repository: paths
|
|
102
|
+
* nothing could be read from, so no name can be cleared against them. One
|
|
103
|
+
* line, with the reasons; the paths are listed in `repo` health.
|
|
104
|
+
*/
|
|
105
|
+
function formatSharedAccountLines(account) {
|
|
106
|
+
const opaque = account && account.opaque;
|
|
107
|
+
if (!opaque || opaque.count === 0) return [];
|
|
108
|
+
const sample = (opaque.sample || []).map(entry => `${entry.path} (${entry.reason})`);
|
|
109
|
+
const more = opaque.count - sample.length;
|
|
110
|
+
return [`WARNING: ${opaque.count} path${opaque.count === 1 ? '' : 's'} could not be searched ` +
|
|
111
|
+
`(${reasonList(opaque.reasons)}), so no name is cleared against ${opaque.count === 1 ? 'it' : 'them'}: ` +
|
|
112
|
+
`${sample.join(', ')}${more > 0 ? `, +${more} more` : ''}. See repo --sections=health.`];
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
/**
|
|
97
116
|
* Render the conservation contract lines: ACCOUNT and CONTRACT (always),
|
|
98
|
-
* WARNING (
|
|
117
|
+
* WARNING (what weakens the claim for THIS symbol), FILTERED (display-filter
|
|
99
118
|
* hides). Returns [] when no account is present (e.g. class-type context).
|
|
119
|
+
*
|
|
120
|
+
* CONTRACT is a state, not a sentence. What the states never mean is the
|
|
121
|
+
* same for every answer and is stated where it is read once (the tool
|
|
122
|
+
* description, the usage guide, `repo` health): a complete text partition
|
|
123
|
+
* is not semantic completeness, and a zero is not deletion proof.
|
|
124
|
+
* text-complete every literal-name line of project
|
|
125
|
+
* source was classified;
|
|
126
|
+
* text-complete, zero observed ... and none is a use;
|
|
127
|
+
* text-complete over indexed source ... except lines in source UCN reads
|
|
128
|
+
* but does not analyze, listed below;
|
|
129
|
+
* degraded some line or path could not be
|
|
130
|
+
* classified at all; see WARNING.
|
|
131
|
+
*
|
|
132
|
+
* `shared: false` leaves out the lines that hold for every symbol of the
|
|
133
|
+
* repository (paths nothing could be read from): a caller that prints many
|
|
134
|
+
* accounts prints those once, through formatSharedAccountLines.
|
|
100
135
|
*/
|
|
101
|
-
function formatAccountLines(account) {
|
|
136
|
+
function formatAccountLines(account, { shared = true } = {}) {
|
|
102
137
|
if (!account) return [];
|
|
103
138
|
const lines = [];
|
|
104
139
|
const nc = account.nonCall || { imports: 0, definitions: 0, references: 0, unclassifiedText: 0, total: 0 };
|
|
@@ -107,7 +142,7 @@ function formatAccountLines(account) {
|
|
|
107
142
|
`${account.confirmed} confirmed, ${account.unverified} unverified, ` +
|
|
108
143
|
`${nc.total} non-call (${nc.imports} import, ${nc.definitions} definition, ${nc.references} reference, ${nc.unclassifiedText} other-text), ` +
|
|
109
144
|
`${account.excluded ? account.excluded.total : 0} other-target, ` +
|
|
110
|
-
`${account.unsupported && account.unsupported.lines > 0 ? `${account.unsupported.lines}
|
|
145
|
+
`${account.unsupported && account.unsupported.lines > 0 ? `${account.unsupported.lines} unanalyzed, ` : ''}` +
|
|
111
146
|
`${account.unaccounted} unaccounted`;
|
|
112
147
|
if (account.beyondText && account.beyondText.count > 0) {
|
|
113
148
|
line += ` (+${account.beyondText.count} beyond-text caller${account.beyondText.count === 1 ? '' : 's'})`;
|
|
@@ -115,31 +150,31 @@ function formatAccountLines(account) {
|
|
|
115
150
|
lines.push(line);
|
|
116
151
|
const contract = account.contract || {};
|
|
117
152
|
const unsupported = account.unsupported ||
|
|
118
|
-
{ fileCount: 0, lines: 0, files: [], languages: {}, sites: [], sitesTruncated: false };
|
|
153
|
+
{ fileCount: 0, lines: 0, files: [], languages: {}, reasons: {}, sites: [], sitesTruncated: false };
|
|
154
|
+
const opaqueCount = account.opaque ? account.opaque.count : 0;
|
|
119
155
|
const textComplete = contract.textComplete !== undefined
|
|
120
156
|
? contract.textComplete
|
|
121
157
|
: Boolean(account.conserved) && !(account.unparsed && account.unparsed.fileCount > 0) &&
|
|
122
158
|
unsupported.lines === 0 &&
|
|
123
|
-
!(account.unreadableFiles && account.unreadableFiles.length > 0)
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
159
|
+
!(account.unreadableFiles && account.unreadableFiles.length > 0) &&
|
|
160
|
+
opaqueCount === 0;
|
|
161
|
+
// When the ONLY gap is occurrences in unanalyzed source, say exactly
|
|
162
|
+
// that: the partition over indexed source is still conserved, and the
|
|
163
|
+
// gap is enumerated below.
|
|
164
|
+
const unanalyzedOnly = !textComplete && unsupported.lines > 0 &&
|
|
128
165
|
Boolean(account.conserved) &&
|
|
129
166
|
!(account.unparsed && account.unparsed.fileCount > 0) &&
|
|
130
|
-
!(account.unreadableFiles && account.unreadableFiles.length > 0)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
lines.push(
|
|
134
|
-
`${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in
|
|
135
|
-
`${langs ? ` (${langs})` : ''} NOT analyzed (see WARNING; verify with grep/ripgrep). ` +
|
|
136
|
-
'Semantic completeness is not claimed.');
|
|
167
|
+
!(account.unreadableFiles && account.unreadableFiles.length > 0) &&
|
|
168
|
+
opaqueCount === 0;
|
|
169
|
+
if (unanalyzedOnly) {
|
|
170
|
+
lines.push('CONTRACT: text-complete over indexed source; ' +
|
|
171
|
+
`${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in unanalyzed source (see WARNING)`);
|
|
137
172
|
} else if (!textComplete) {
|
|
138
|
-
lines.push('CONTRACT:
|
|
173
|
+
lines.push('CONTRACT: degraded; see WARNING');
|
|
139
174
|
} else if (contract.observedTextZero) {
|
|
140
|
-
lines.push('CONTRACT:
|
|
175
|
+
lines.push('CONTRACT: text-complete, zero observed; not deletion proof');
|
|
141
176
|
} else {
|
|
142
|
-
lines.push('CONTRACT:
|
|
177
|
+
lines.push('CONTRACT: text-complete');
|
|
143
178
|
}
|
|
144
179
|
if (account.unparsed && account.unparsed.fileCount > 0) {
|
|
145
180
|
lines.push(`WARNING: ${account.unparsed.fileCount} unparsed file${account.unparsed.fileCount === 1 ? '' : 's'} ` +
|
|
@@ -156,12 +191,11 @@ function formatAccountLines(account) {
|
|
|
156
191
|
`${rec.lines > shown.length ? `, ... and ${rec.lines - shown.length} more` : ''}`);
|
|
157
192
|
}
|
|
158
193
|
if (unsupported.lines > 0) {
|
|
159
|
-
const
|
|
160
|
-
.map(([lang, count]) => `${lang}: ${count}`).join(', ');
|
|
194
|
+
const kinds = matchLabelCounts(unsupported);
|
|
161
195
|
lines.push(`WARNING: ${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in ` +
|
|
162
|
-
`${unsupported.fileCount}
|
|
196
|
+
`${unsupported.fileCount} unanalyzed file${unsupported.fileCount === 1 ? '' : 's'} ` +
|
|
163
197
|
`contain${unsupported.lines === 1 ? 's' : ''} "${account.symbol}"` +
|
|
164
|
-
`${
|
|
198
|
+
`${kinds ? ` (${kinds})` : ''} — NOT analyzed; verify with grep/ripgrep:`);
|
|
165
199
|
const shown = unsupported.sites.slice(0, 5);
|
|
166
200
|
for (const site of shown) {
|
|
167
201
|
lines.push(` ${site.file}:${site.line} ${site.text}`);
|
|
@@ -173,14 +207,7 @@ function formatAccountLines(account) {
|
|
|
173
207
|
if (account.unreadableFiles && account.unreadableFiles.length > 0) {
|
|
174
208
|
lines.push(`WARNING: ${account.unreadableFiles.length} unreadable file(s) skipped: ${account.unreadableFiles.join(', ')}`);
|
|
175
209
|
}
|
|
176
|
-
if (
|
|
177
|
-
const shown = account.skippedSources.slice(0, 5)
|
|
178
|
-
.map(issue => `${issue.relativePath} (${issue.reason})`);
|
|
179
|
-
lines.push(`WARNING: ${account.skippedSources.length} source discovery gap(s) make this index partial: ${shown.join(', ')}`);
|
|
180
|
-
if (account.skippedSources.length > shown.length) {
|
|
181
|
-
lines.push(` ... and ${account.skippedSources.length - shown.length} more; run repo --sections=health for the full reason set.`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
210
|
+
if (shared) lines.push(...formatSharedAccountLines(account));
|
|
184
211
|
if (account.filtered && account.filtered.total > 0) {
|
|
185
212
|
const parts = [];
|
|
186
213
|
const f = account.filtered.byFlag || {};
|
|
@@ -192,6 +219,15 @@ function formatAccountLines(account) {
|
|
|
192
219
|
return lines;
|
|
193
220
|
}
|
|
194
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Rows an unverified list prints: every one with `all`, `top` when it was
|
|
224
|
+
* given, ten otherwise. The header always states the full count.
|
|
225
|
+
*/
|
|
226
|
+
function unverifiedRowCap(options) {
|
|
227
|
+
if (options?.all) return Infinity;
|
|
228
|
+
return Number.isInteger(options?.top) && options.top > 0 ? options.top : paramFact('top', 'unverifiedRows');
|
|
229
|
+
}
|
|
230
|
+
|
|
195
231
|
/**
|
|
196
232
|
* Single-node CALLEE ACCOUNT line (context/smart — the per-def callee
|
|
197
233
|
* contract from #223). Matches the tree rollup's arithmetic phrasing.
|
|
@@ -328,9 +364,7 @@ function formatContextJson(context) {
|
|
|
328
364
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
329
365
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
330
366
|
...(c.externalContract && { externalContract: true }),
|
|
331
|
-
uncertaintyClass: c
|
|
332
|
-
(c.reason === 'possible-dispatch' && c.dispatchVia
|
|
333
|
-
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
367
|
+
uncertaintyClass: uncertaintyClassOf(c),
|
|
334
368
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
335
369
|
})),
|
|
336
370
|
...(context.ambiguityCandidates && {
|
|
@@ -391,9 +425,7 @@ function formatContextJson(context) {
|
|
|
391
425
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
392
426
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
393
427
|
...(c.externalContract && { externalContract: true }),
|
|
394
|
-
uncertaintyClass: c
|
|
395
|
-
(c.reason === 'possible-dispatch' && c.dispatchVia
|
|
396
|
-
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
428
|
+
uncertaintyClass: uncertaintyClassOf(c),
|
|
397
429
|
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
398
430
|
})),
|
|
399
431
|
...(context.ambiguityCandidates && {
|
|
@@ -506,7 +538,9 @@ function formatContext(ctx, options = {}) {
|
|
|
506
538
|
if (typeUnverified.length > 0) {
|
|
507
539
|
lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
508
540
|
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
509
|
-
const
|
|
541
|
+
const typeReasons = reasonSummaryLine(typeUnverified, options.evidenceBands);
|
|
542
|
+
if (typeReasons) lines.push(typeReasons);
|
|
543
|
+
const cap = unverifiedRowCap(ctx.meta);
|
|
510
544
|
let shown = 0;
|
|
511
545
|
for (const u of typeUnverified) {
|
|
512
546
|
if (shown >= cap) break;
|
|
@@ -628,20 +662,15 @@ function formatContext(ctx, options = {}) {
|
|
|
628
662
|
// This separates unavoidable language semantics from ambiguity the agent
|
|
629
663
|
// can investigate.
|
|
630
664
|
const unverified = ctx.unverifiedCallers || [];
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const actionableUnverified = unverified.filter(u =>
|
|
637
|
-
u.uncertaintyClass !== 'compile-time-dispatch' &&
|
|
638
|
-
!(u.uncertaintyClass === 'runtime-dispatch' ||
|
|
639
|
-
(u.reason === 'possible-dispatch' && u.dispatchVia)));
|
|
665
|
+
// One classification for the text, the JSON and the review counts
|
|
666
|
+
// (core/review-items.js).
|
|
667
|
+
const {
|
|
668
|
+
runtime: runtimeDispatch, compileTime: compileTimeDispatch, actionable: actionableUnverified,
|
|
669
|
+
} = partitionUnverified(unverified);
|
|
640
670
|
if (runtimeDispatch.length > 0) {
|
|
641
671
|
const groups = new Map();
|
|
642
672
|
for (const site of runtimeDispatch) {
|
|
643
|
-
const key =
|
|
644
|
-
`${site.externalContract ? 'external' : 'project'}`;
|
|
673
|
+
const key = runtimeFamilyKey(site);
|
|
645
674
|
if (!groups.has(key)) groups.set(key, []);
|
|
646
675
|
groups.get(key).push(site);
|
|
647
676
|
}
|
|
@@ -732,11 +761,13 @@ function formatContext(ctx, options = {}) {
|
|
|
732
761
|
}
|
|
733
762
|
|
|
734
763
|
// Actionable ambiguity: call syntax without enough identity evidence.
|
|
735
|
-
// Always visible
|
|
764
|
+
// Always visible; ten one-liners unless --top or --all says otherwise.
|
|
736
765
|
if (actionableUnverified.length > 0) {
|
|
737
766
|
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
738
767
|
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
739
|
-
const
|
|
768
|
+
const reasons = reasonSummaryLine(actionableUnverified, options.evidenceBands);
|
|
769
|
+
if (reasons) lines.push(reasons);
|
|
770
|
+
const cap = unverifiedRowCap(ctx.meta);
|
|
740
771
|
let shown = 0;
|
|
741
772
|
for (const u of actionableUnverified) {
|
|
742
773
|
if (shown >= cap) break;
|
|
@@ -862,6 +893,8 @@ function formatImpact(impact, options = {}) {
|
|
|
862
893
|
}
|
|
863
894
|
|
|
864
895
|
const compact = !!options.compact;
|
|
896
|
+
const allHint = options.allHint || 'use --all';
|
|
897
|
+
const unverifiedCap = unverifiedRowCap(options);
|
|
865
898
|
const lines = [];
|
|
866
899
|
|
|
867
900
|
// Header
|
|
@@ -970,14 +1003,14 @@ function formatImpact(impact, options = {}) {
|
|
|
970
1003
|
const reason = impact.propertyAccesses ? 'matching attribute syntax, receiver identity unresolved'
|
|
971
1004
|
: 'matching value syntax, binding identity unresolved';
|
|
972
1005
|
lines.push(`${compact ? '' : '\n'}UNVERIFIED ${label} ACCESS CANDIDATES (${access.unverifiedSites.length}) — ${reason}:`);
|
|
973
|
-
for (const site of access.unverifiedSites.slice(0,
|
|
1006
|
+
for (const site of access.unverifiedSites.slice(0, unverifiedCap)) {
|
|
974
1007
|
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
975
1008
|
const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
976
1009
|
const why = impact.valueAccesses && site.reason ? ` (${site.reason})` : '';
|
|
977
1010
|
lines.push(` ${site.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}${why}`);
|
|
978
1011
|
}
|
|
979
|
-
if (access.unverifiedSites.length >
|
|
980
|
-
lines.push(` (+${access.unverifiedSites.length -
|
|
1012
|
+
if (access.unverifiedSites.length > unverifiedCap) {
|
|
1013
|
+
lines.push(` (+${access.unverifiedSites.length - unverifiedCap} more unverified — ${allHint})`);
|
|
981
1014
|
}
|
|
982
1015
|
}
|
|
983
1016
|
const valueAccount = impact.valueAccesses?.account;
|
|
@@ -999,21 +1032,23 @@ function formatImpact(impact, options = {}) {
|
|
|
999
1032
|
}
|
|
1000
1033
|
if (refs.unverifiedSites.length > 0) {
|
|
1001
1034
|
lines.push(`${compact ? '' : '\n'}UNVERIFIED TYPE REFERENCE CANDIDATES (${refs.unverifiedSites.length}) — the name, the type lookup cannot decide what it names:`);
|
|
1002
|
-
for (const site of refs.unverifiedSites.slice(0,
|
|
1035
|
+
for (const site of refs.unverifiedSites.slice(0, unverifiedCap)) {
|
|
1003
1036
|
const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
1004
1037
|
lines.push(` ${site.file}:${site.line} [${position(site)}]${expr} (${site.reason})`);
|
|
1005
1038
|
}
|
|
1006
|
-
if (refs.unverifiedSites.length >
|
|
1007
|
-
lines.push(` (+${refs.unverifiedSites.length -
|
|
1039
|
+
if (refs.unverifiedSites.length > unverifiedCap) {
|
|
1040
|
+
lines.push(` (+${refs.unverifiedSites.length - unverifiedCap} more unverified — ${allHint})`);
|
|
1008
1041
|
}
|
|
1009
1042
|
}
|
|
1010
1043
|
if (refs.account) lines.push(formatTypeAccount(impact.function, refs));
|
|
1011
1044
|
}
|
|
1012
1045
|
|
|
1013
|
-
// Unverified tier: visible
|
|
1046
|
+
// Unverified tier: visible; ten one-liners unless --top or --all says otherwise
|
|
1014
1047
|
if (impactUnverified.length > 0) {
|
|
1015
1048
|
lines.push(`${compact ? '' : '\n'}UNVERIFIED CALL SITES (${impactUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
1016
|
-
const
|
|
1049
|
+
const reasons = reasonSummaryLine(impactUnverified, options.evidenceBands);
|
|
1050
|
+
if (reasons) lines.push(reasons);
|
|
1051
|
+
const cap = unverifiedCap;
|
|
1017
1052
|
for (const site of impactUnverified.slice(0, cap)) {
|
|
1018
1053
|
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
1019
1054
|
const reason = site.reason ? ` (${unverifiedReasonLabel(site)})` : '';
|
|
@@ -1021,7 +1056,7 @@ function formatImpact(impact, options = {}) {
|
|
|
1021
1056
|
lines.push(` ${site.file}:${site.line}${caller}${expr}${reason}`);
|
|
1022
1057
|
}
|
|
1023
1058
|
if (impactUnverified.length > cap) {
|
|
1024
|
-
lines.push(` (+${impactUnverified.length - cap} more unverified)`);
|
|
1059
|
+
lines.push(` (+${impactUnverified.length - cap} more unverified — ${allHint})`);
|
|
1025
1060
|
}
|
|
1026
1061
|
}
|
|
1027
1062
|
|
|
@@ -1171,6 +1206,8 @@ function formatAbout(about, options = {}) {
|
|
|
1171
1206
|
if (aboutUnverified && aboutUnverified.total > 0) {
|
|
1172
1207
|
lines.push('');
|
|
1173
1208
|
lines.push(`CALLERS — UNVERIFIED (${aboutUnverified.total}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
1209
|
+
const reasons = reasonSummaryLine(aboutUnverified.reasons || aboutUnverified.top, options.evidenceBands);
|
|
1210
|
+
if (reasons) lines.push(reasons);
|
|
1174
1211
|
for (const u of aboutUnverified.top) {
|
|
1175
1212
|
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
1176
1213
|
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
@@ -1325,6 +1362,7 @@ module.exports = {
|
|
|
1325
1362
|
// Shared with output/tracing.js: the tree commands render the same
|
|
1326
1363
|
// root-hop ACCOUNT/WARNING/FILTERED lines as context/impact.
|
|
1327
1364
|
formatAccountLines,
|
|
1365
|
+
formatSharedAccountLines,
|
|
1328
1366
|
// Shared with output/analysis-ext.js (smart): single-node callee contract.
|
|
1329
1367
|
formatCalleeAccountLine,
|
|
1330
1368
|
unverifiedCalleeLines,
|