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
package/core/output/check.js
CHANGED
|
@@ -1,14 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* core/output/check.js — Pre-commit check formatter.
|
|
3
|
+
*
|
|
4
|
+
* Items are grouped by verdict, worst first: what broke, what to look at,
|
|
5
|
+
* what nothing depends on. Every site of a break is listed: a reader who
|
|
6
|
+
* has to fix thirteen call sites needs thirteen lines.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
'use strict';
|
|
6
10
|
|
|
11
|
+
const SECTIONS = [
|
|
12
|
+
['blocked', 'BLOCKED', 'a break was observed'],
|
|
13
|
+
['review', 'REVIEW', 'change exposure or call requirements not settled'],
|
|
14
|
+
['ready', 'READY', 'no unresolved exposure in the modeled checks'],
|
|
15
|
+
];
|
|
16
|
+
// Occurrences of a name are context, not a worklist: a head is enough.
|
|
17
|
+
const NAME_SITE_LIMIT = 10;
|
|
18
|
+
|
|
19
|
+
function callersText(it) {
|
|
20
|
+
if (it.kind === 'module') return '';
|
|
21
|
+
let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
22
|
+
if (it.unverifiedCallerCount > 0) callers += ` (+${it.unverifiedCallerCount} unverified)`;
|
|
23
|
+
return callers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function itemLines(it) {
|
|
27
|
+
const tags = [];
|
|
28
|
+
if (it.kind && it.kind !== 'changed') tags.push(it.kind.toUpperCase());
|
|
29
|
+
if (it.verdict === 'ready' && it.kind === 'modified') {
|
|
30
|
+
if (it.change === 'inert') tags.push('contract unchanged');
|
|
31
|
+
else if (it.noDependents) tags.push('no static dependents');
|
|
32
|
+
}
|
|
33
|
+
if (it.verdict !== 'ready') {
|
|
34
|
+
for (const reason of it.reasons || []) if (reason !== 'signature-mismatch') tags.push(reason);
|
|
35
|
+
}
|
|
36
|
+
if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
|
|
37
|
+
if (it.orphan) tags.push('ORPHAN');
|
|
38
|
+
if (it.symbolType) tags.push(it.symbolType);
|
|
39
|
+
// Caller evidence matters where the callers do.
|
|
40
|
+
if (it.verdict !== 'ready' && it.kind !== 'deleted' && it.account && !it.account.textComplete) {
|
|
41
|
+
tags.push('ACCOUNT-INCOMPLETE');
|
|
42
|
+
}
|
|
43
|
+
const lines = [` ${it.name} (${it.file}:${it.line})${tags.length ? ` [${tags.join(', ')}]` : ''} ${callersText(it)}`.trimEnd()];
|
|
44
|
+
if (it.symbolType) {
|
|
45
|
+
lines.push(` Dependencies: ${it.dependencyCount || 0} confirmed, ${it.unverifiedDependencyCount || 0} unverified`);
|
|
46
|
+
}
|
|
47
|
+
for (const m of it.mismatches || []) {
|
|
48
|
+
const problem = m.expected ? `expected ${m.expected}, got ${m.actual}` : (m.reason || 'arity mismatch');
|
|
49
|
+
const where = m.file ? `${m.file}:${m.line}` : '';
|
|
50
|
+
const expression = m.expression ? ` ${String(m.expression).trim()}` : '';
|
|
51
|
+
lines.push(` ↳ ${where}${expression} ${problem}${m.unchecked ? ' (not checked by the toolchain)' : ''}`);
|
|
52
|
+
}
|
|
53
|
+
for (const site of it.outgoingReview || []) {
|
|
54
|
+
lines.push(` ↳ ${site.file}:${site.line}${site.column != null ? `:${site.column + 1}` : ''} ${site.name}: ${site.reason}`);
|
|
55
|
+
}
|
|
56
|
+
const sites = it.sites || [];
|
|
57
|
+
const definite = it.verdict === 'blocked';
|
|
58
|
+
const shown = definite ? sites : sites.slice(0, NAME_SITE_LIMIT);
|
|
59
|
+
for (const site of shown) {
|
|
60
|
+
lines.push(` ↳ ${site.file}${site.line ? `:${site.line}` : ''}${site.kind ? ` ${site.kind}` : ''}` +
|
|
61
|
+
`${site.unchecked ? ' (not checked by the toolchain)' : ''}`);
|
|
62
|
+
}
|
|
63
|
+
if (sites.length > shown.length) lines.push(` (+${sites.length - shown.length} more occurrence(s))`);
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
7
67
|
function formatCheck(result) {
|
|
8
68
|
if (!result) return 'No check result.';
|
|
9
69
|
if (result.ok === false) {
|
|
10
70
|
// The gate could not run — never render this like a clean tree.
|
|
11
|
-
|
|
71
|
+
const advice = result.status === 'staged-snapshot-unavailable'
|
|
72
|
+
? 'This is not a pass, and says nothing about the worktree. Resolve what keeps the git index from being exported (unmerged paths, a missing object) and rerun.'
|
|
73
|
+
: 'This is not a pass. Fix the git context (run inside a git repository with a valid base ref) and rerun.';
|
|
74
|
+
return `Pre-commit Check (${result.base}${result.staged ? ', staged' : ''})\n${'═'.repeat(60)}\nCHECK DID NOT RUN [${result.status || 'diff-failed'}] — ${result.error || 'git diff failed'}\n${advice}`;
|
|
12
75
|
}
|
|
13
76
|
const pathNotes = [];
|
|
14
77
|
if (result.nonSourcePaths > 0) pathNotes.push(`Note: ${result.nonSourcePaths} changed path(s) outside supported source files not analyzed.`);
|
|
@@ -21,20 +84,26 @@ function formatCheck(result) {
|
|
|
21
84
|
lines.push(`Pre-commit Check vs ${result.base}${result.staged ? ' (staged)' : ''}`);
|
|
22
85
|
lines.push('═'.repeat(60));
|
|
23
86
|
lines.push(...pathNotes);
|
|
87
|
+
for (const note of result.notes || []) lines.push(`Note: ${note}`);
|
|
88
|
+
const items = result.changed || [];
|
|
24
89
|
if (result.trust) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
90
|
+
const trust = result.trust;
|
|
91
|
+
lines.push(`TRUST: ${trust.status} — UCN evidence is not semantic proof; compiler and tests are required`);
|
|
92
|
+
const counts = [];
|
|
93
|
+
if (trust.blocked) counts.push(`${trust.blocked} blocked`);
|
|
94
|
+
if (trust.review) counts.push(`${trust.review} to review`);
|
|
95
|
+
if (trust.ready) counts.push(`${trust.ready} ready`);
|
|
96
|
+
const details = [];
|
|
97
|
+
if (trust.signatureMismatches) details.push(`${trust.signatureMismatches} signature mismatch(es)`);
|
|
98
|
+
if (trust.unverifiedCallSites) details.push(`${trust.unverifiedCallSites} unverified caller(s)`);
|
|
99
|
+
if (trust.incompleteAccounts) details.push(`${trust.incompleteAccounts} incomplete account(s)`);
|
|
100
|
+
if (trust.filteredEdges) details.push(`${trust.filteredEdges} filtered edge(s)`);
|
|
101
|
+
if (trust.usageReviewSymbols) details.push(`${trust.usageReviewSymbols} symbol(s) need usages review`);
|
|
102
|
+
if (trust.unvalidatedDeclarations) details.push(`${trust.unvalidatedDeclarations} declaration change(s) need toolchain validation`);
|
|
103
|
+
if (trust.strict) details.push('strict: review does not pass');
|
|
104
|
+
if (counts.length > 0 || details.length > 0) lines.push(` ${[...counts, ...details].join(' · ')}`);
|
|
34
105
|
}
|
|
35
106
|
|
|
36
|
-
// Changed functions section
|
|
37
|
-
const items = result.changed || [];
|
|
38
107
|
const noun = items.some(it => it.symbolType) ? 'symbol' : 'function';
|
|
39
108
|
if (result.truncated) {
|
|
40
109
|
lines.push(`Changed: ${items.length} of ${result.totalChanged} ${noun}s`);
|
|
@@ -44,38 +113,28 @@ function formatCheck(result) {
|
|
|
44
113
|
if (items.length === 0) {
|
|
45
114
|
lines.push(' (none — only non-function changes)');
|
|
46
115
|
} else {
|
|
47
|
-
for (const
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (it.symbolType) tags.push(it.symbolType);
|
|
53
|
-
if (it.account && !it.account.textComplete) tags.push('ACCOUNT-INCOMPLETE');
|
|
54
|
-
const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
|
|
55
|
-
let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
56
|
-
if (it.unverifiedCallerCount > 0) {
|
|
57
|
-
callers += ` (+${it.unverifiedCallerCount} unverified)`;
|
|
58
|
-
}
|
|
59
|
-
lines.push(` ${it.name} (${it.file}:${it.line})${tagStr} ${callers}`);
|
|
60
|
-
if (it.symbolType) lines.push(` Dependencies: ${it.dependencyCount || 0} confirmed, ${it.unverifiedDependencyCount || 0} unverified`);
|
|
61
|
-
if (it.mismatches && it.mismatches.length > 0) {
|
|
62
|
-
for (const m of it.mismatches.slice(0, 3)) {
|
|
63
|
-
const where = m.file ? ` at ${m.file}:${m.line}` : '';
|
|
64
|
-
const reason = m.expected
|
|
65
|
-
? `expected ${m.expected}, got ${m.actual}`
|
|
66
|
-
: (m.reason || 'arity mismatch');
|
|
67
|
-
lines.push(` ↳ ${reason}${where}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
116
|
+
for (const [verdict, title, meaning] of SECTIONS) {
|
|
117
|
+
const group = items.filter(it => (it.verdict || 'review') === verdict);
|
|
118
|
+
if (group.length === 0) continue;
|
|
119
|
+
lines.push(`${title} (${group.length}) — ${meaning}:`);
|
|
120
|
+
for (const it of group) lines.push(...itemLines(it));
|
|
70
121
|
}
|
|
71
122
|
}
|
|
72
123
|
|
|
73
124
|
// Tests
|
|
74
125
|
lines.push('');
|
|
75
126
|
if (result.testFiles && result.testFiles.length > 0) {
|
|
76
|
-
|
|
127
|
+
const files = result.testFiles.length;
|
|
128
|
+
// A count of tests is printed only when tests were counted: files
|
|
129
|
+
// whose tests are not read have none to report.
|
|
130
|
+
// "at least": a file among them holds tests that are not listed.
|
|
131
|
+
const conditional = result.conditionalTests ? ` (${result.conditionalTests} conditional)` : '';
|
|
132
|
+
lines.push(result.totalTests > 0
|
|
133
|
+
? `Tests potentially affected: ${result.moreTests ? 'at least ' : ''}${result.totalTests} in ${files} file${files === 1 ? '' : 's'}${conditional}`
|
|
134
|
+
: `Test files potentially affected: ${files}${result.moreTests ? ' (tests not listed)' : ''}`);
|
|
77
135
|
for (const tf of result.testFiles.slice(0, 8)) {
|
|
78
|
-
const cnt = tf.testCount ? ` (${tf.
|
|
136
|
+
const cnt = tf.testCount ? ` (${tf.moreTests ? 'at least ' : ''}${tf.testCount})`
|
|
137
|
+
: tf.moreTests ? ' (tests not listed)' : '';
|
|
79
138
|
lines.push(` ${tf.file}${cnt}`);
|
|
80
139
|
}
|
|
81
140
|
if (result.testFiles.length > 8) {
|
|
@@ -84,6 +143,11 @@ function formatCheck(result) {
|
|
|
84
143
|
} else {
|
|
85
144
|
lines.push('Tests: none detected for changed functions');
|
|
86
145
|
}
|
|
146
|
+
if (result.supportFiles && result.supportFiles.length > 0) {
|
|
147
|
+
const names = result.supportFiles.slice(0, 5).map(row => row.file).join(', ');
|
|
148
|
+
const more = result.supportFiles.length > 5 ? `, +${result.supportFiles.length - 5} more` : '';
|
|
149
|
+
lines.push(`Test support code reached (declares no test, runs with the tests that load it): ${names}${more}`);
|
|
150
|
+
}
|
|
87
151
|
|
|
88
152
|
// Action items
|
|
89
153
|
if (result.actions && result.actions.length > 0) {
|
package/core/output/doctor.js
CHANGED
|
@@ -116,7 +116,15 @@ function formatDoctor(result, options = {}) {
|
|
|
116
116
|
const reasons = Object.entries(info.reasons || {})
|
|
117
117
|
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
118
118
|
lines.push(` ${label}: ${fileCount} path${fileCount === 1 ? '' : 's'} not indexed${reasons ? ` (${reasons})` : ''}`);
|
|
119
|
-
|
|
119
|
+
const unsearchable = info.unsearchable || 0;
|
|
120
|
+
if (unsearchable > 0) {
|
|
121
|
+
lines.push(` ${unsearchable} could not be searched: no caller account is complete until ` +
|
|
122
|
+
'they can be read (fix the path, raise the limit, or declare it in .ucn.json outsideProject).');
|
|
123
|
+
}
|
|
124
|
+
if (fileCount > unsearchable) {
|
|
125
|
+
lines.push(` ${unsearchable > 0 ? 'The rest is searched' : 'Searched'} as text for each name: ` +
|
|
126
|
+
'an occurrence there is listed, not analyzed.');
|
|
127
|
+
}
|
|
120
128
|
} else {
|
|
121
129
|
lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
122
130
|
}
|
|
@@ -129,6 +137,31 @@ function formatDoctor(result, options = {}) {
|
|
|
129
137
|
}
|
|
130
138
|
if (!anyBlindSpot) lines.push(' (none detected)');
|
|
131
139
|
|
|
140
|
+
// What a completeness claim ranges over and what it never means: said
|
|
141
|
+
// here once, so an answer only carries its own state.
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('Contract scope:');
|
|
144
|
+
const scope = result.scope;
|
|
145
|
+
if (scope) {
|
|
146
|
+
const list = reasons => Object.entries(reasons || {})
|
|
147
|
+
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
148
|
+
lines.push(` Indexed source, plus ${scope.textOnly.count} file${scope.textOnly.count === 1 ? '' : 's'} ` +
|
|
149
|
+
`searched as text for each name${scope.textOnly.count > 0 ? ` (${list(scope.textOnly.reasons)})` : ''}.`);
|
|
150
|
+
if (scope.opaque.count > 0) {
|
|
151
|
+
lines.push(` Not searchable: ${scope.opaque.count} path${scope.opaque.count === 1 ? '' : 's'} ` +
|
|
152
|
+
`(${list(scope.opaque.reasons)}); every caller account is degraded until they are.`);
|
|
153
|
+
}
|
|
154
|
+
if (scope.outside.count > 0) {
|
|
155
|
+
const sample = scope.outside.sample.slice(0, 3).map(entry => entry.path).join(', ');
|
|
156
|
+
lines.push(` Outside the project: ${scope.outside.count} path${scope.outside.count === 1 ? '' : 's'} ` +
|
|
157
|
+
`(${list(scope.outside.reasons)}), no part of any claim: ${sample}` +
|
|
158
|
+
`${scope.outside.count > 3 ? ', ...' : ''}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
lines.push(' CONTRACT text-complete: every literal-name line in that scope was classified.');
|
|
162
|
+
lines.push(' It is never semantic completeness (aliases, indirect calls, generated code and');
|
|
163
|
+
lines.push(' runtime dispatch may exist), and a zero is never deletion proof.');
|
|
164
|
+
|
|
132
165
|
// Task-specific readiness. One scalar cannot honestly describe navigation,
|
|
133
166
|
// refactoring, and deletion risk.
|
|
134
167
|
lines.push('');
|
package/core/output/find.js
CHANGED
|
@@ -10,9 +10,10 @@ const {
|
|
|
10
10
|
} = require('./shared');
|
|
11
11
|
|
|
12
12
|
const { formatSymbolHandle } = require('../shared');
|
|
13
|
+
const { paramFact } = require('../registry');
|
|
13
14
|
|
|
14
15
|
// Detailed text rows shown by default; the rest are counted.
|
|
15
|
-
const DEFAULT_DISPLAY_LIMIT =
|
|
16
|
+
const DEFAULT_DISPLAY_LIMIT = paramFact('limit', 'findDetail');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Trim a docstring to a single short sentence (max 80 chars) for inline display.
|
package/core/output/lines.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { CALLABLE_SYMBOL_KINDS } = require('../shared');
|
|
18
|
-
const { formatSurfaceMessage } = require('../registry');
|
|
18
|
+
const { CANONICAL_COMMANDS, FLAG_APPLICABILITY, formatSurfaceMessage } = require('../registry');
|
|
19
19
|
const { formatAccountLines, formatCalleeAccountLine } = require('./analysis');
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// The commands that take `lines`: the registry's applicability.
|
|
22
|
+
const LINES_COMMANDS = new Set(CANONICAL_COMMANDS.filter(command => FLAG_APPLICABILITY[command].includes('lines')));
|
|
22
23
|
|
|
23
24
|
function record(pathLike, line, text, tag = '') {
|
|
24
25
|
// One record per line is the contract: a multi-line signature or a
|
|
@@ -304,4 +305,63 @@ function formatPublicRaw(result, execution = {}) {
|
|
|
304
305
|
return code;
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
|
|
308
|
+
/**
|
|
309
|
+
* A line listing cut to `limit` characters: the records that fit, in their
|
|
310
|
+
* order, then every comment line, then one `# truncated` line counting the
|
|
311
|
+
* records left out. A record is never cut and a comment line is never
|
|
312
|
+
* dropped: the accounting still describes the whole answer. Null when the
|
|
313
|
+
* listing has no record to drop or when not one record fits beside the
|
|
314
|
+
* comment lines. `allowNoRows` may return no rows, but still keeps every
|
|
315
|
+
* comment; if that cannot fit, it explicitly withholds the whole answer.
|
|
316
|
+
*/
|
|
317
|
+
function budgetLineListing(text, limit, surface = 'cli', { allowNoRows = false } = {}) {
|
|
318
|
+
const lines = String(text).split('\n').filter(line => line !== '');
|
|
319
|
+
const records = lines.filter(line => !line.startsWith('# '));
|
|
320
|
+
const comments = lines.filter(line => line.startsWith('# '));
|
|
321
|
+
if (records.length === 0 && !allowNoRows) return null;
|
|
322
|
+
const advice = surface === 'mcp'
|
|
323
|
+
? 'raise max_chars or narrow the query'
|
|
324
|
+
: 'raise --max-chars or omit it';
|
|
325
|
+
const trailer = omitted => `# truncated: ${omitted} of ${records.length} rows omitted; ${advice}`;
|
|
326
|
+
// The most records whose text, with every comment line and the trailer
|
|
327
|
+
// that counts the rest, is within the limit.
|
|
328
|
+
const fixed = comments.reduce((sum, line) => sum + line.length + 1, 0);
|
|
329
|
+
const withheld = () => {
|
|
330
|
+
const flag = surface === 'mcp' ? 'max_chars' : '--max-chars';
|
|
331
|
+
const needed = Math.min(String(text).length, fixed + trailer(records.length).length);
|
|
332
|
+
const notices = [
|
|
333
|
+
`# Answer withheld: accounting does not fit; raise ${flag} to at least ${needed}.`,
|
|
334
|
+
`# Answer withheld; ${flag}>=${needed}`,
|
|
335
|
+
`# withheld; >=${needed}`,
|
|
336
|
+
];
|
|
337
|
+
return {
|
|
338
|
+
text: notices.find(notice => notice.length <= limit) || '',
|
|
339
|
+
rows: records.length, omitted: records.length, contractMetadataComplete: false,
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
if (records.length === 0) return withheld();
|
|
343
|
+
const prefix = [0];
|
|
344
|
+
for (const record of records) prefix.push(prefix[prefix.length - 1] + record.length + 1);
|
|
345
|
+
let kept = 0;
|
|
346
|
+
for (let count = records.length - 1; count >= 1; count--) {
|
|
347
|
+
if (prefix[count] + fixed + trailer(records.length - count).length <= limit) { kept = count; break; }
|
|
348
|
+
}
|
|
349
|
+
if (kept === 0) {
|
|
350
|
+
// No row fits. Never present a partial accounting as the complete
|
|
351
|
+
// answer: all comment lines accompany the row count, or the entire
|
|
352
|
+
// answer is withheld explicitly.
|
|
353
|
+
const all = trailer(records.length);
|
|
354
|
+
if (!allowNoRows) return null;
|
|
355
|
+
if (fixed + all.length > limit) return withheld();
|
|
356
|
+
return { text: [...comments, all].join('\n'), rows: records.length, omitted: records.length,
|
|
357
|
+
contractMetadataComplete: true };
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
text: [...records.slice(0, kept), ...comments, trailer(records.length - kept)].join('\n'),
|
|
361
|
+
rows: records.length,
|
|
362
|
+
omitted: records.length - kept,
|
|
363
|
+
contractMetadataComplete: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = { LINES_COMMANDS, formatPublicLines, formatPublicRaw, commentLines, budgetLineListing };
|
package/core/output/public.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
const { COMMAND_CONTRACTS } = require('../command-contracts');
|
|
12
12
|
const { COMMAND_TRUST_MATRIX } = require('../trust-matrix');
|
|
13
|
-
const { toCliName, toMcpName, formatSurfaceMessage } = require('../registry');
|
|
13
|
+
const { toCliName, toMcpName, formatSurfaceMessage, paramDefault } = require('../registry');
|
|
14
14
|
const { LINES_COMMANDS, formatPublicLines, formatPublicRaw } = require('./lines');
|
|
15
15
|
const { provenanceReplacer } = require('./provenance');
|
|
16
16
|
|
|
17
|
+
const { summarizeReasons } = require('../evidence-bands');
|
|
18
|
+
|
|
17
19
|
const legacy = {
|
|
18
20
|
...require('./analysis'),
|
|
19
21
|
...require('./analysis-ext'),
|
|
@@ -163,11 +165,12 @@ function formatShow(result, params = {}, hints = presentationHints()) {
|
|
|
163
165
|
if (result.context) {
|
|
164
166
|
const context = { ...result.context };
|
|
165
167
|
const formatted = legacy.formatContext(context, {
|
|
166
|
-
showConfidence: params.showConfidence
|
|
167
|
-
compact: params.compact
|
|
168
|
+
showConfidence: params.showConfidence ?? paramDefault('showConfidence'),
|
|
169
|
+
compact: params.compact ?? paramDefault('compact', 'show'),
|
|
168
170
|
expandHint: hints.source,
|
|
169
171
|
allHint: hints.all,
|
|
170
172
|
usagesHint: hints.usages(context.function || result.target),
|
|
173
|
+
evidenceBands: hints.evidenceBands,
|
|
171
174
|
});
|
|
172
175
|
parts.push(block('RELATIONSHIPS', projectContextText(formatted.text, selected)));
|
|
173
176
|
}
|
|
@@ -227,10 +230,14 @@ function formatTrace(result, params = {}, hints = presentationHints()) {
|
|
|
227
230
|
});
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
function formatImpact(result, params = {}) {
|
|
233
|
+
function formatImpact(result, params = {}, hints = presentationHints()) {
|
|
234
|
+
// `--all` prints every unverified row, `--top` (or `--limit`) that many.
|
|
235
|
+
const top = Number(params.limit) > 0 ? Number(params.limit) : Number(params.top) > 0 ? Number(params.top) : undefined;
|
|
236
|
+
const rows = { all: !!params.all, ...(Number.isInteger(top) && { top }), allHint: hints.all,
|
|
237
|
+
evidenceBands: hints.evidenceBands };
|
|
231
238
|
return modeOf('impact', result) === 'diff'
|
|
232
|
-
? legacy.formatDiffImpact(result,
|
|
233
|
-
: legacy.formatImpact(result, { compact: params.compact
|
|
239
|
+
? legacy.formatDiffImpact(result, rows)
|
|
240
|
+
: legacy.formatImpact(result, { compact: params.compact ?? paramDefault('compact', 'impact'), ...rows });
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
function formatTests(result, params = {}, hints = presentationHints()) {
|
|
@@ -299,11 +306,13 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
299
306
|
if (shaped != null) return shaped;
|
|
300
307
|
}
|
|
301
308
|
const hints = presentationHints(execution.surface);
|
|
309
|
+
// Display only: another band table relabels the reason summaries.
|
|
310
|
+
if (execution.evidenceBands) hints.evidenceBands = execution.evidenceBands;
|
|
302
311
|
if (result?.scopeWarning?.hint) {
|
|
303
|
-
result
|
|
312
|
+
result = { ...result, scopeWarning: {
|
|
304
313
|
...result.scopeWarning,
|
|
305
314
|
hint: formatSurfaceMessage(result.scopeWarning.hint, execution.surface),
|
|
306
|
-
};
|
|
315
|
+
} };
|
|
307
316
|
}
|
|
308
317
|
let text;
|
|
309
318
|
switch (command) {
|
|
@@ -312,7 +321,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
312
321
|
text = modeOf('find', result) === 'type'
|
|
313
322
|
? legacy.formatTypedef(result, params.name)
|
|
314
323
|
: legacy.formatFindDetailed(result, params.name, {
|
|
315
|
-
compact: params.compact,
|
|
324
|
+
compact: params.compact ?? paramDefault('compact', 'find'),
|
|
316
325
|
withSource: params.withSource,
|
|
317
326
|
top: params.limit,
|
|
318
327
|
all: !!params.all,
|
|
@@ -320,7 +329,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
320
329
|
});
|
|
321
330
|
break;
|
|
322
331
|
case 'usages': text = legacy.formatUsages(result, params.name, {
|
|
323
|
-
compact: params.compact,
|
|
332
|
+
compact: params.compact ?? paramDefault('compact', 'usages'),
|
|
324
333
|
all: params.all,
|
|
325
334
|
allHint: hints.all,
|
|
326
335
|
}); break;
|
|
@@ -341,7 +350,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
341
350
|
break;
|
|
342
351
|
case 'source': text = formatSource(result, hints); break;
|
|
343
352
|
case 'trace': text = formatTrace(result, params, hints); break;
|
|
344
|
-
case 'impact': text = formatImpact(result, params); break;
|
|
353
|
+
case 'impact': text = formatImpact(result, params, hints); break;
|
|
345
354
|
case 'tests': text = formatTests(result, params, hints); break;
|
|
346
355
|
case 'deps': text = formatDeps(result, params, hints); break;
|
|
347
356
|
case 'api': text = legacy.formatApi(result, params.file || '.'); break;
|
|
@@ -413,6 +422,8 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
413
422
|
commandMeta.excludedConstructors = result.excludedConstructors || 0;
|
|
414
423
|
commandMeta.pythonImplicitExportFiles = result.pythonImplicitExportFiles || 0;
|
|
415
424
|
commandMeta.excludedDynamicDispatch = result.excludedDynamicDispatch || 0;
|
|
425
|
+
commandMeta.excludedRequiredSlot = result.excludedRequiredSlot || 0;
|
|
426
|
+
commandMeta.projectKind = result.projectKind || 'library';
|
|
416
427
|
commandMeta.computedDispatch = result.computedDispatch || { count: 0, names: [] };
|
|
417
428
|
commandMeta.reflection = result.reflection || {
|
|
418
429
|
count: 0, literalCount: 0, dynamicCount: 0,
|
|
@@ -468,6 +479,13 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
468
479
|
if (command === 'usages' && result?.analysisGaps) {
|
|
469
480
|
commandMeta.analysisGaps = result.analysisGaps;
|
|
470
481
|
}
|
|
482
|
+
// The reasons of the unverified callers with how often each was a real
|
|
483
|
+
// edge on the release board: labels beside the data, never part of it.
|
|
484
|
+
const unverifiedCallers = command === 'impact' ? result?.unverifiedSites
|
|
485
|
+
: command === 'show' ? result?.context?.unverifiedCallers : null;
|
|
486
|
+
if (Array.isArray(unverifiedCallers) && unverifiedCallers.length > 0) {
|
|
487
|
+
commandMeta.unverifiedReasons = summarizeReasons(unverifiedCallers, execution.evidenceBands);
|
|
488
|
+
}
|
|
471
489
|
|
|
472
490
|
const surfaceCommand = execution.surface === 'mcp'
|
|
473
491
|
? toMcpName(command)
|
|
@@ -71,10 +71,42 @@ function groupedReviewLines(items, cap = 20) {
|
|
|
71
71
|
* Format plan command output - text
|
|
72
72
|
* Shows before/after signatures and all changes needed
|
|
73
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* A deletion assessment: the verdict, its basis and where the declaration
|
|
76
|
+
* is. What the verdict never means (a guarantee) is stated once, in the
|
|
77
|
+
* tool description and the usage guide.
|
|
78
|
+
*/
|
|
79
|
+
function formatDeletion(assessment) {
|
|
80
|
+
const d = assessment.declaration;
|
|
81
|
+
const owner = d.className ? `${d.className}.` : '';
|
|
82
|
+
const lines = [
|
|
83
|
+
`Deletion assessment: ${owner}${d.name}`,
|
|
84
|
+
'═'.repeat(60),
|
|
85
|
+
`${d.file}:${d.startLine}${d.type ? ` (${d.type})` : ''}`,
|
|
86
|
+
...(assessment.warnings || []).map(warning => `Note: ${warning.message}`),
|
|
87
|
+
`VERDICT: ${assessment.verdict}`,
|
|
88
|
+
` basis: ${assessment.basis}`,
|
|
89
|
+
];
|
|
90
|
+
if (assessment.site) {
|
|
91
|
+
const site = assessment.site;
|
|
92
|
+
lines.push(` first use: ${site.file}${site.line != null ? `:${site.line}` : ''}`);
|
|
93
|
+
}
|
|
94
|
+
if (assessment.projectKind === 'application') {
|
|
95
|
+
lines.push(' project kind: application (.ucn.json): exported symbols are assessed');
|
|
96
|
+
}
|
|
97
|
+
if (assessment.verdict === 'has-static-dependents') {
|
|
98
|
+
lines.push(` next: ucn show ${d.file}:${d.startLine}:${d.name} lists the callers; ucn usages ${d.name} lists every occurrence`);
|
|
99
|
+
} else if (assessment.reason === 'downstream-exposure') {
|
|
100
|
+
lines.push(' next: declare "kind": "application" in .ucn.json if nothing outside this repository consumes it');
|
|
101
|
+
}
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
function formatPlan(plan, options = {}) {
|
|
75
106
|
if (!plan) {
|
|
76
107
|
return 'Function not found.';
|
|
77
108
|
}
|
|
109
|
+
if (plan.operation === 'delete') return formatDeletion(plan);
|
|
78
110
|
if (!plan.found) {
|
|
79
111
|
return `Function "${plan.function}" not found.`;
|
|
80
112
|
}
|
package/core/output/reporting.js
CHANGED
|
@@ -215,8 +215,11 @@ function formatDeadcode(results, options = {}) {
|
|
|
215
215
|
if (results.length === 0 && !results.excludedDecorated &&
|
|
216
216
|
!results.excludedExported && !results.excludedExternalContract &&
|
|
217
217
|
!results.excludedRuntimeContract && !results.excludedConstructors && !results.pythonImplicitExportFiles &&
|
|
218
|
-
!results.excludedDynamicDispatch && !results.
|
|
218
|
+
!results.excludedDynamicDispatch && !results.excludedRequiredSlot &&
|
|
219
|
+
results.projectKind !== 'application' && !results.computedDispatch?.count &&
|
|
219
220
|
!results.reflection?.withheld && !results.reflection?.dynamicCount && !results.reflection?.literalCount &&
|
|
221
|
+
!results.reflection?.withheldByTable?.length && !results.macroPaste?.withheld &&
|
|
222
|
+
!results.macroPaste?.blind?.count && !results.macroRulesBlind?.count &&
|
|
220
223
|
results.coverage?.complete !== false) {
|
|
221
224
|
return 'No dead code found.';
|
|
222
225
|
}
|
|
@@ -232,6 +235,8 @@ function formatDeadcode(results, options = {}) {
|
|
|
232
235
|
} else {
|
|
233
236
|
lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
|
|
234
237
|
}
|
|
238
|
+
} else {
|
|
239
|
+
lines.push('Dead code: 0 unused symbol(s)\n');
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
let currentFile = null;
|
|
@@ -301,6 +306,12 @@ function formatDeadcode(results, options = {}) {
|
|
|
301
306
|
if (results.excludedDynamicDispatch > 0) {
|
|
302
307
|
lines.push(`\n${results.excludedDynamicDispatch} registry member(s) hidden because a matching computed dispatch (registry[key]()) may invoke them.`);
|
|
303
308
|
}
|
|
309
|
+
if (results.excludedRequiredSlot > 0) {
|
|
310
|
+
lines.push(`\n${results.excludedRequiredSlot} member(s) withheld: required by an interface, abstract class or trait of the project, overridden by a descendant that says so, or matching a project interface or protocol by shape. Removing one can break a type.`);
|
|
311
|
+
}
|
|
312
|
+
if (results.projectKind === 'application') {
|
|
313
|
+
lines.push('\nProject kind: application (.ucn.json). Exported symbols are audited; contracts, registrations and reflection still withhold.');
|
|
314
|
+
}
|
|
304
315
|
if (results.computedDispatch?.count > 0) {
|
|
305
316
|
lines.push(`\nWARNING: ${results.computedDispatch.count} computed dispatch call(s) in ${results.computedDispatch.fileCount} file(s). Dead-code results are review candidates; runtime-selected members may not have a named static edge.`);
|
|
306
317
|
}
|
|
@@ -379,6 +390,8 @@ function formatDeadcodeJson(results) {
|
|
|
379
390
|
...(results.excludedConstructors > 0 && { excludedConstructors: results.excludedConstructors }),
|
|
380
391
|
...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
|
|
381
392
|
...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
|
|
393
|
+
...(results.excludedRequiredSlot > 0 && { excludedRequiredSlot: results.excludedRequiredSlot }),
|
|
394
|
+
...(results.projectKind === 'application' && { projectKind: results.projectKind }),
|
|
382
395
|
...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
|
|
383
396
|
...(results.reflection?.count > 0 && { reflection: results.reflection }),
|
|
384
397
|
...(results.macroPaste && { macroPaste: results.macroPaste }),
|
|
@@ -573,7 +586,11 @@ function formatOrient(result, options = {}) {
|
|
|
573
586
|
if (result.skippedSources?.count > 0) {
|
|
574
587
|
const reasons = Object.entries(result.skippedSources.reasons || {})
|
|
575
588
|
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
576
|
-
|
|
589
|
+
const unsearchable = result.skippedSources.unsearchable || 0;
|
|
590
|
+
lines.push(`SKIPPED SOURCE: ${result.skippedSources.count} path(s) not indexed${reasons ? ` (${reasons})` : ''} — ` +
|
|
591
|
+
(unsearchable > 0
|
|
592
|
+
? `${unsearchable} could not be searched, so no caller account is complete.`
|
|
593
|
+
: 'searched as text for each name; an occurrence there is listed, not analyzed.'));
|
|
577
594
|
}
|
|
578
595
|
lines.push('');
|
|
579
596
|
|
package/core/output/search.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { detectDoubleEscaping, advisoryLine } = require('./shared');
|
|
6
|
+
const { selectorText, testCountText } = require('../test-units');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Format search command output
|
|
@@ -419,15 +420,26 @@ function formatTests(tests, name) {
|
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
for (const testFile of tests) {
|
|
422
|
-
|
|
423
|
+
const counted = testCountText(testFile.tests, testFile.moreTests, testFile.conditionalTests);
|
|
424
|
+
const facts = testFile.role === 'support' ? ' [support: declares no test]'
|
|
425
|
+
: counted ? ` (${counted})` : '';
|
|
426
|
+
lines.push(`${testFile.file}${facts}`);
|
|
423
427
|
for (const match of testFile.matches) {
|
|
424
428
|
const typeLabel = match.matchType === 'test-case' ? '[test]' :
|
|
425
429
|
match.matchType === 'import' ? '[import]' :
|
|
426
430
|
match.matchType === 'call' ? '[call]' :
|
|
427
431
|
match.matchType === 'unverified-call' ? '[unverified-call]' :
|
|
428
432
|
match.matchType === 'unverified-reference' ? '[unverified-ref]' :
|
|
433
|
+
match.matchType === 'inherited-test' ? '[inherited]' :
|
|
429
434
|
match.matchType === 'string-ref' ? '[string]' : '[ref]';
|
|
430
|
-
|
|
435
|
+
// A test the class inherits: its name, and where its body is.
|
|
436
|
+
const origin = match.matchType === 'inherited-test' ? ` ${match.name} (from ${match.from})` : '';
|
|
437
|
+
lines.push(` ${match.line}: ${typeLabel} ${match.content}${origin}`);
|
|
438
|
+
}
|
|
439
|
+
if (testFile.run) lines.push(` run: ${selectorText(testFile.run)}`);
|
|
440
|
+
else if (testFile.role === 'tests' && testFile.runWithheld) lines.push(` no command: ${testFile.runWithheld}`);
|
|
441
|
+
for (const unit of testFile.units || []) {
|
|
442
|
+
if (unit.selector) lines.push(` run test: ${selectorText(unit.selector)}`);
|
|
431
443
|
}
|
|
432
444
|
lines.push('');
|
|
433
445
|
}
|