ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public v5 presentation router.
|
|
5
|
+
*
|
|
6
|
+
* CLI and MCP both call this module after the shared execute() path. The
|
|
7
|
+
* engine's narrower internal results remain independently testable, while the
|
|
8
|
+
* public surface has one formatter decision for each canonical command.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { COMMAND_CONTRACTS } = require('../command-contracts');
|
|
12
|
+
const { COMMAND_TRUST_MATRIX } = require('../trust-matrix');
|
|
13
|
+
const { toCliName, toMcpName, formatSurfaceMessage } = require('../registry');
|
|
14
|
+
|
|
15
|
+
const legacy = {
|
|
16
|
+
...require('./analysis'),
|
|
17
|
+
...require('./analysis-ext'),
|
|
18
|
+
...require('./brief'),
|
|
19
|
+
...require('./check'),
|
|
20
|
+
...require('./doctor'),
|
|
21
|
+
...require('./endpoints'),
|
|
22
|
+
...require('./extraction'),
|
|
23
|
+
...require('./find'),
|
|
24
|
+
...require('./graph'),
|
|
25
|
+
...require('./refactoring'),
|
|
26
|
+
...require('./reporting'),
|
|
27
|
+
...require('./search'),
|
|
28
|
+
...require('./tracing'),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function block(title, text) {
|
|
32
|
+
if (!text) return '';
|
|
33
|
+
return `${title}\n${'─'.repeat(Math.min(60, title.length || 1))}\n${text}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendNote(text, note) {
|
|
37
|
+
return note ? `${text}\n\n${note}` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Canonicalize object keys so JSON bytes do not depend on index provenance. */
|
|
41
|
+
function canonicalJsonValue(value) {
|
|
42
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
43
|
+
if (!value || typeof value !== 'object') return value;
|
|
44
|
+
const canonical = {};
|
|
45
|
+
for (const key of Object.keys(value).sort()) {
|
|
46
|
+
canonical[key] = canonicalJsonValue(value[key]);
|
|
47
|
+
}
|
|
48
|
+
return canonical;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function contractMeta(command) {
|
|
52
|
+
const contract = COMMAND_CONTRACTS[command];
|
|
53
|
+
const trust = COMMAND_TRUST_MATRIX[command];
|
|
54
|
+
if (!contract || !trust) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
question: contract.question,
|
|
57
|
+
decisionSafety: trust.decisionSafety,
|
|
58
|
+
truth: contract.truth,
|
|
59
|
+
next: contract.next,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function modeOf(command, result) {
|
|
64
|
+
if (result && result._publicMode) return result._publicMode;
|
|
65
|
+
if (command === 'impact') return result && Array.isArray(result.functions) ? 'diff' : 'symbol';
|
|
66
|
+
if (command === 'trace') return result && result.direction === 'down' ? 'callees' : 'callers';
|
|
67
|
+
if (command === 'tests') return result && result.root ? 'affected' : 'direct';
|
|
68
|
+
if (command === 'check') return result && result.expectedArgs ? 'symbol' : 'diff';
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function presentationHints(surface = 'cli') {
|
|
73
|
+
if (surface === 'mcp') {
|
|
74
|
+
return {
|
|
75
|
+
surface: 'mcp',
|
|
76
|
+
all: 'use all=true',
|
|
77
|
+
limit: 'Use limit=<n> to return and display more results.',
|
|
78
|
+
source: 'Run command=source with name=<handle> to inspect a listed symbol.',
|
|
79
|
+
usages: name => `run command=usages with name=${name}`,
|
|
80
|
+
detailed: 'Use detailed=true to list all functions and classes.',
|
|
81
|
+
top: 'Use top=<n> or all=true to show more.',
|
|
82
|
+
health: 'run command=repo with sections=health and deep=true for detail',
|
|
83
|
+
expandUnverified: 'expand_unverified=true',
|
|
84
|
+
includeMethods: 'Use include_methods=true to show them.',
|
|
85
|
+
nextRepo: result => [
|
|
86
|
+
...(result.suggest ? [`command=show name=${result.suggest}`] : []),
|
|
87
|
+
'command=repo sections=files detailed=true',
|
|
88
|
+
'command=repo sections=stats hot=true top=20',
|
|
89
|
+
'command=repo sections=health deep=true',
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
surface: 'cli',
|
|
95
|
+
all: 'use --all',
|
|
96
|
+
limit: 'Use --limit=N to return and display more results.',
|
|
97
|
+
source: 'Use source <handle> to inspect a listed symbol.',
|
|
98
|
+
usages: name => `ucn usages ${name}`,
|
|
99
|
+
detailed: 'Use --detailed to list all functions and classes.',
|
|
100
|
+
top: 'Use --top=N or --all to show more.',
|
|
101
|
+
health: 'ucn repo --sections=health --deep for detail',
|
|
102
|
+
expandUnverified: '--expand-unverified',
|
|
103
|
+
includeMethods: 'Use --include-methods to show them.',
|
|
104
|
+
nextRepo: result => [
|
|
105
|
+
...(result.suggest ? [`ucn show ${result.suggest}`] : []),
|
|
106
|
+
'ucn repo --sections=files --detailed',
|
|
107
|
+
'ucn repo --sections=stats --hot --top=20',
|
|
108
|
+
'ucn repo --sections=health --deep',
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatSource(result, hints = presentationHints()) {
|
|
114
|
+
const extractionHints = hints.surface === 'mcp'
|
|
115
|
+
? {
|
|
116
|
+
maxLinesHint: 'use max_lines=<n>, or omit it for the full function',
|
|
117
|
+
classSourceHint: 'Use max_lines=<n> to see source, or run command=source with name=<method-handle> for an individual method.',
|
|
118
|
+
}
|
|
119
|
+
: {};
|
|
120
|
+
const mode = modeOf('source', result);
|
|
121
|
+
if (mode === 'lines' || (result && Array.isArray(result.lines))) {
|
|
122
|
+
return legacy.formatLines(result);
|
|
123
|
+
}
|
|
124
|
+
if (mode === 'class') return legacy.formatClassResult(result, extractionHints);
|
|
125
|
+
return legacy.formatFnResult(result, extractionHints);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function projectContextText(text, selected) {
|
|
129
|
+
if (!text) return text;
|
|
130
|
+
const keepCallers = selected.has('callers');
|
|
131
|
+
const keepCallees = selected.has('callees');
|
|
132
|
+
if (keepCallers && keepCallees) return text;
|
|
133
|
+
const lines = text.split('\n');
|
|
134
|
+
const projected = [];
|
|
135
|
+
let band = null;
|
|
136
|
+
const metadata = /^(?:NON-CALL OCCURRENCES|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT):/;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (/^CALLERS —/.test(line)) band = 'callers';
|
|
139
|
+
else if (/^CALLEES(?: —| \()/.test(line)) band = 'callees';
|
|
140
|
+
else if (metadata.test(line)) band = null;
|
|
141
|
+
if ((band === 'callers' && !keepCallers) ||
|
|
142
|
+
(band === 'callees' && !keepCallees)) continue;
|
|
143
|
+
projected.push(line);
|
|
144
|
+
}
|
|
145
|
+
return projected.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatShow(result, params = {}, hints = presentationHints()) {
|
|
149
|
+
const selected = new Set(result.sections || []);
|
|
150
|
+
const parts = [];
|
|
151
|
+
|
|
152
|
+
if (result.summary) parts.push(block('SUMMARY', legacy.formatBrief(result.summary)));
|
|
153
|
+
if (result.context) {
|
|
154
|
+
const context = { ...result.context };
|
|
155
|
+
const formatted = legacy.formatContext(context, {
|
|
156
|
+
showConfidence: params.showConfidence !== false,
|
|
157
|
+
compact: params.compact !== false,
|
|
158
|
+
expandHint: hints.source,
|
|
159
|
+
allHint: hints.all,
|
|
160
|
+
usagesHint: hints.usages(context.function || result.target),
|
|
161
|
+
});
|
|
162
|
+
parts.push(block('RELATIONSHIPS', projectContextText(formatted.text, selected)));
|
|
163
|
+
}
|
|
164
|
+
if (result.source) parts.push(block('SOURCE', formatSource(result.source, hints)));
|
|
165
|
+
if (result.dependencies) parts.push(block('DEPENDENCIES', legacy.formatSmart(result.dependencies)));
|
|
166
|
+
if (result.tests) {
|
|
167
|
+
parts.push(block('TESTS', formatTests(
|
|
168
|
+
result.tests,
|
|
169
|
+
{ ...params, depth: 0 },
|
|
170
|
+
hints,
|
|
171
|
+
)));
|
|
172
|
+
}
|
|
173
|
+
if (result.types) {
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const types = (result.types.types || []).map(type => ({
|
|
176
|
+
...type,
|
|
177
|
+
relativePath: type.relativePath || type.file,
|
|
178
|
+
startLine: type.startLine || type.line,
|
|
179
|
+
})).filter(type => {
|
|
180
|
+
const key = `${type.name}\0${type.type}\0${type.relativePath}\0${type.startLine}`;
|
|
181
|
+
if (seen.has(key)) return false;
|
|
182
|
+
seen.add(key);
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
parts.push(block('TYPES', legacy.formatTypedef(types, result.target)));
|
|
186
|
+
}
|
|
187
|
+
if (result.example) parts.push(block('EXAMPLE', legacy.formatExample(result.example, result.target)));
|
|
188
|
+
if (result.related) parts.push(block('RELATED', legacy.formatRelated(result.related, {
|
|
189
|
+
all: params.all,
|
|
190
|
+
top: params.top,
|
|
191
|
+
allHint: hints.all,
|
|
192
|
+
})));
|
|
193
|
+
|
|
194
|
+
return parts.join('\n\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatTrace(result, params = {}, hints = presentationHints()) {
|
|
198
|
+
const traceHints = {
|
|
199
|
+
allHint: hints.all,
|
|
200
|
+
expandUnverifiedHint: hints.expandUnverified,
|
|
201
|
+
includeMethodsHint: hints.includeMethods,
|
|
202
|
+
};
|
|
203
|
+
const mode = modeOf('trace', result);
|
|
204
|
+
if (mode === 'entrypoints') return legacy.formatReverseTrace(result, {
|
|
205
|
+
...traceHints,
|
|
206
|
+
allHint: 'Increase depth for a wider path search.',
|
|
207
|
+
});
|
|
208
|
+
if (mode === 'callers') return legacy.formatBlast(result, {
|
|
209
|
+
...traceHints,
|
|
210
|
+
allHint: hints.surface === 'mcp'
|
|
211
|
+
? 'Use all=true to lift the per-node child cap; depth controls hops only.'
|
|
212
|
+
: 'Use --all to lift the per-node child cap; --depth controls hops only.',
|
|
213
|
+
});
|
|
214
|
+
return legacy.formatTrace(result, {
|
|
215
|
+
...traceHints,
|
|
216
|
+
allHint: 'Increase depth for a wider callee tree.',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatImpact(result, params = {}) {
|
|
221
|
+
return modeOf('impact', result) === 'diff'
|
|
222
|
+
? legacy.formatDiffImpact(result, { all: params.all })
|
|
223
|
+
: legacy.formatImpact(result, { compact: params.compact !== false });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatTests(result, params = {}, hints = presentationHints()) {
|
|
227
|
+
return modeOf('tests', result) === 'affected'
|
|
228
|
+
? legacy.formatAffectedTests(result, { all: params.all, allHint: hints.all })
|
|
229
|
+
: legacy.formatTests(result, params.name);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatDeps(result, params = {}, hints = presentationHints()) {
|
|
233
|
+
if (modeOf('deps', result) === 'cycles' || result.cycles) {
|
|
234
|
+
return legacy.formatCircularDeps(result);
|
|
235
|
+
}
|
|
236
|
+
const parts = [legacy.formatGraph(result.graph, {
|
|
237
|
+
showAll: params.all || params.depth != null,
|
|
238
|
+
maxDepth: params.depth ?? 2,
|
|
239
|
+
file: result.file,
|
|
240
|
+
depthHint: hints.surface === 'mcp'
|
|
241
|
+
? 'Use depth=<n> for a deeper graph.'
|
|
242
|
+
: 'Use --depth=N for a deeper graph.',
|
|
243
|
+
allHint: hints.all,
|
|
244
|
+
})];
|
|
245
|
+
if (result.imports) parts.push(block('IMPORT DECLARATIONS', legacy.formatImports(result.imports, result.file)));
|
|
246
|
+
if (result.importers) parts.push(block('IMPORTERS', legacy.formatExporters(result.importers, result.file)));
|
|
247
|
+
return parts.join('\n\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatRepo(result, params = {}, hints = presentationHints()) {
|
|
251
|
+
const parts = [];
|
|
252
|
+
if (result.summary) {
|
|
253
|
+
parts.push(legacy.formatOrient(result.summary, {
|
|
254
|
+
healthHint: hints.health,
|
|
255
|
+
nextHints: hints.nextRepo,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
if (result.files) {
|
|
259
|
+
parts.push(block('FILES', legacy.formatToc(result.files, {
|
|
260
|
+
detailedHint: hints.detailed,
|
|
261
|
+
topHint: hints.top,
|
|
262
|
+
})));
|
|
263
|
+
}
|
|
264
|
+
if (result.stats) parts.push(block('STATISTICS', legacy.formatStats(result.stats, {
|
|
265
|
+
top: params.top || 0,
|
|
266
|
+
topHint: hints.surface === 'mcp' ? 'use top=<n> to show more' : 'use --top=N to show more',
|
|
267
|
+
})));
|
|
268
|
+
if (result.health) {
|
|
269
|
+
parts.push(block('HEALTH', legacy.formatDoctor(result.health, {
|
|
270
|
+
deepHint: hints.surface === 'mcp' ? 'use deep=true' : 'use --deep',
|
|
271
|
+
})));
|
|
272
|
+
}
|
|
273
|
+
return parts.join('\n\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatPublicText(command, result, params = {}, execution = {}) {
|
|
277
|
+
const hints = presentationHints(execution.surface);
|
|
278
|
+
if (result?.scopeWarning?.hint) {
|
|
279
|
+
result.scopeWarning = {
|
|
280
|
+
...result.scopeWarning,
|
|
281
|
+
hint: formatSurfaceMessage(result.scopeWarning.hint, execution.surface),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let text;
|
|
285
|
+
switch (command) {
|
|
286
|
+
case 'show': text = formatShow(result, params, hints); break;
|
|
287
|
+
case 'find':
|
|
288
|
+
text = modeOf('find', result) === 'type'
|
|
289
|
+
? legacy.formatTypedef(result, params.name)
|
|
290
|
+
: legacy.formatFindDetailed(result, params.name, {
|
|
291
|
+
compact: params.compact,
|
|
292
|
+
withSource: params.withSource,
|
|
293
|
+
top: params.limit,
|
|
294
|
+
all: !!params.all,
|
|
295
|
+
limitHint: hints.limit,
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
case 'usages': text = legacy.formatUsages(result, params.name, {
|
|
299
|
+
compact: params.compact,
|
|
300
|
+
all: params.all,
|
|
301
|
+
allHint: hints.all,
|
|
302
|
+
}); break;
|
|
303
|
+
case 'search':
|
|
304
|
+
text = execution.structural || result?.meta?.mode === 'structural'
|
|
305
|
+
? legacy.formatStructuralSearch(result, {
|
|
306
|
+
topHint: hints.surface === 'mcp'
|
|
307
|
+
? 'Use top=<n> to see more.' : 'Use --top=N to see more.',
|
|
308
|
+
unusedFlag: hints.surface === 'mcp' ? 'unused=true' : '--unused',
|
|
309
|
+
})
|
|
310
|
+
: legacy.formatSearch(result, params.term, {
|
|
311
|
+
topHint: hints.surface === 'mcp'
|
|
312
|
+
? 'Use top=<n> to see more.' : 'Use --top=N to see more.',
|
|
313
|
+
includeTestsHint: hints.surface === 'mcp'
|
|
314
|
+
? 'use include_tests=true to include'
|
|
315
|
+
: 'use --include-tests to include',
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
case 'source': text = formatSource(result, hints); break;
|
|
319
|
+
case 'trace': text = formatTrace(result, params, hints); break;
|
|
320
|
+
case 'impact': text = formatImpact(result, params); break;
|
|
321
|
+
case 'tests': text = formatTests(result, params, hints); break;
|
|
322
|
+
case 'deps': text = formatDeps(result, params, hints); break;
|
|
323
|
+
case 'api': text = legacy.formatApi(result, params.file || '.'); break;
|
|
324
|
+
case 'check':
|
|
325
|
+
text = modeOf('check', result) === 'symbol'
|
|
326
|
+
? legacy.formatVerify(result)
|
|
327
|
+
: legacy.formatCheck(result);
|
|
328
|
+
break;
|
|
329
|
+
case 'plan': text = legacy.formatPlan(result, { surface: execution.surface }); break;
|
|
330
|
+
case 'repo': text = formatRepo(result, params, hints); break;
|
|
331
|
+
case 'deadcode': text = legacy.formatDeadcode(result, {
|
|
332
|
+
top: params.top || 0,
|
|
333
|
+
topHint: hints.top,
|
|
334
|
+
...(hints.surface === 'mcp' && {
|
|
335
|
+
decoratedHint: `${result.excludedDecorated || 0} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.`,
|
|
336
|
+
exportedHint: `${result.excludedExported || 0} exported symbol(s) excluded from the audit (public API may have external callers). Use include_exported=true to audit them.`,
|
|
337
|
+
externalContractHint: `${result.excludedExternalContract || 0} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use include_exported=true to include them.`,
|
|
338
|
+
}),
|
|
339
|
+
}); break;
|
|
340
|
+
case 'entrypoints': text = legacy.formatEntrypoints(result); break;
|
|
341
|
+
case 'endpoints': text = legacy.formatEndpoints(result, {
|
|
342
|
+
bridge: result._bridge,
|
|
343
|
+
unmatched: result._unmatched,
|
|
344
|
+
serverOnly: result._serverOnly,
|
|
345
|
+
clientOnly: result._clientOnly,
|
|
346
|
+
}); break;
|
|
347
|
+
case 'stacktrace': text = legacy.formatStackTrace(result); break;
|
|
348
|
+
case 'auditAsync': text = legacy.formatAuditAsync(result); break;
|
|
349
|
+
default: throw new Error(`No public formatter for command: ${command}`);
|
|
350
|
+
}
|
|
351
|
+
return appendNote(text, execution.note
|
|
352
|
+
? formatSurfaceMessage(execution.note, execution.surface)
|
|
353
|
+
: execution.note);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
357
|
+
let commandMeta = {};
|
|
358
|
+
let data = result;
|
|
359
|
+
|
|
360
|
+
// Ambiguous bare-name resolution is a surface-level trust decision. Keep
|
|
361
|
+
// it in the common envelope even for array results (notably `tests`, whose
|
|
362
|
+
// auxiliary metadata is intentionally non-enumerable).
|
|
363
|
+
if (result?.warnings?.length > 0) {
|
|
364
|
+
commandMeta.warnings = result.warnings;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Keep the stable public envelope while reusing the endpoint serializer's
|
|
368
|
+
// trimmed records and aggregate metadata. This prevents private routing
|
|
369
|
+
// fields from leaking and preserves the established machine contract.
|
|
370
|
+
if (command === 'endpoints') {
|
|
371
|
+
const formatted = JSON.parse(legacy.formatEndpointsJson(result, {
|
|
372
|
+
bridge: result?._bridge,
|
|
373
|
+
unmatched: result?._unmatched,
|
|
374
|
+
}));
|
|
375
|
+
commandMeta = formatted.meta || {};
|
|
376
|
+
data = formatted.data;
|
|
377
|
+
}
|
|
378
|
+
if (command === 'deadcode' && Array.isArray(result)) {
|
|
379
|
+
commandMeta.deletionSafety = 'review-required';
|
|
380
|
+
commandMeta.excludedExported = result.excludedExported || 0;
|
|
381
|
+
commandMeta.excludedDecorated = result.excludedDecorated || 0;
|
|
382
|
+
commandMeta.excludedExternalContract = result.excludedExternalContract || 0;
|
|
383
|
+
commandMeta.excludedRuntimeContract = result.excludedRuntimeContract || 0;
|
|
384
|
+
commandMeta.pythonImplicitExportFiles = result.pythonImplicitExportFiles || 0;
|
|
385
|
+
commandMeta.excludedDynamicDispatch = result.excludedDynamicDispatch || 0;
|
|
386
|
+
commandMeta.computedDispatch = result.computedDispatch || { count: 0, names: [] };
|
|
387
|
+
if (result.coverage) commandMeta.coverage = result.coverage;
|
|
388
|
+
if (result.limitInfo) {
|
|
389
|
+
commandMeta.total = result.limitInfo.total;
|
|
390
|
+
commandMeta.truncated = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (command === 'search' && result?.meta) {
|
|
394
|
+
commandMeta.searchMode = result.meta.mode;
|
|
395
|
+
commandMeta.totalMatches = result.meta.totalMatches;
|
|
396
|
+
commandMeta.shownMatches = result.meta.shownMatches;
|
|
397
|
+
commandMeta.truncatedMatches = result.meta.truncatedMatches;
|
|
398
|
+
commandMeta.limit = result.meta.limit;
|
|
399
|
+
if (result.meta.truncatedMatches > 0) commandMeta.truncated = true;
|
|
400
|
+
if (result.unsupportedMatches) commandMeta.unsupportedMatches = result.unsupportedMatches;
|
|
401
|
+
}
|
|
402
|
+
if (command === 'entrypoints' && result?.filterInfo) {
|
|
403
|
+
commandMeta.hiddenTestEntrypoints = result.filterInfo.hiddenTests;
|
|
404
|
+
commandMeta.testsIncluded = result.filterInfo.testsIncluded;
|
|
405
|
+
if (result.limitInfo) {
|
|
406
|
+
commandMeta.total = result.limitInfo.total;
|
|
407
|
+
commandMeta.truncated = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Handles are the documented spine (`Pass the resulting handle to show/
|
|
411
|
+
// impact/source`) — the JSON records must carry them, not make agents
|
|
412
|
+
// concatenate relativePath:startLine:name themselves.
|
|
413
|
+
if (command === 'find' && Array.isArray(result)) {
|
|
414
|
+
const { formatSymbolHandle } = require('../shared');
|
|
415
|
+
if (result.findInfo) {
|
|
416
|
+
commandMeta.total = result.findInfo.total;
|
|
417
|
+
commandMeta.shown = result.findInfo.shown;
|
|
418
|
+
if (result.findInfo.shown < result.findInfo.total) commandMeta.truncated = true;
|
|
419
|
+
}
|
|
420
|
+
data = result.map(item => {
|
|
421
|
+
const handle = formatSymbolHandle(item);
|
|
422
|
+
return handle ? { handle, ...item } : item;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// Mixed-language disclosure for commands whose result is not
|
|
426
|
+
// account-shaped: the counts must be machine-readable, not note-only.
|
|
427
|
+
if ((command === 'find' || command === 'usages' || command === 'tests') &&
|
|
428
|
+
result?.unsupportedMatches) {
|
|
429
|
+
commandMeta.unsupportedMatches = result.unsupportedMatches;
|
|
430
|
+
}
|
|
431
|
+
if (command === 'usages' && result?.analysisGaps) {
|
|
432
|
+
commandMeta.analysisGaps = result.analysisGaps;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const surfaceCommand = execution.surface === 'mcp'
|
|
436
|
+
? toMcpName(command)
|
|
437
|
+
: execution.surface === 'cli' ? toCliName(command) : command;
|
|
438
|
+
|
|
439
|
+
const envelope = {
|
|
440
|
+
meta: {
|
|
441
|
+
command: surfaceCommand,
|
|
442
|
+
...(surfaceCommand !== command && { canonicalCommand: command }),
|
|
443
|
+
// A result that self-reports it could not run (check with a bad
|
|
444
|
+
// base ref / outside git) must be machine-distinguishable from a
|
|
445
|
+
// successful empty result at the envelope level too.
|
|
446
|
+
...(data && data.ok === false && { ok: false }),
|
|
447
|
+
...(modeOf(command, result) && { mode: modeOf(command, result) }),
|
|
448
|
+
contract: contractMeta(command),
|
|
449
|
+
...commandMeta,
|
|
450
|
+
...(execution.note && { note: execution.note }),
|
|
451
|
+
},
|
|
452
|
+
data,
|
|
453
|
+
};
|
|
454
|
+
return JSON.stringify(canonicalJsonValue(envelope), null, 2);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
module.exports = {
|
|
458
|
+
formatPublicText,
|
|
459
|
+
formatPublicJson,
|
|
460
|
+
contractMeta,
|
|
461
|
+
modeOf,
|
|
462
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { unverifiedReasonLabel, advisoryLine } = require('./shared');
|
|
6
6
|
const { formatAccountLines } = require('./analysis');
|
|
7
|
+
const { formatSurfaceMessage } = require('../registry');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Render the v4 unverified band shared by verify and plan: capped one-liners
|
|
@@ -41,9 +42,10 @@ function formatPlan(plan, options = {}) {
|
|
|
41
42
|
// Only show the parameter list when the error result carries one —
|
|
42
43
|
// unrelated errors (multi-op rejection) don't, and "none" would be
|
|
43
44
|
// wrong for functions that have parameters.
|
|
45
|
+
const nativeError = formatSurfaceMessage(plan.error, options.surface || 'cli');
|
|
44
46
|
return plan.currentParams
|
|
45
|
-
? `Error: ${
|
|
46
|
-
: `Error: ${
|
|
47
|
+
? `Error: ${nativeError}\nCurrent parameters: ${plan.currentParams.join(', ') || 'none'}`
|
|
48
|
+
: `Error: ${nativeError}`;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const lines = [];
|
|
@@ -52,6 +54,9 @@ function formatPlan(plan, options = {}) {
|
|
|
52
54
|
lines.push(`Refactoring plan: ${plan.operation}`);
|
|
53
55
|
lines.push('═'.repeat(60));
|
|
54
56
|
lines.push(`${plan.file}:${plan.startLine}`);
|
|
57
|
+
for (const warning of plan.warnings || []) {
|
|
58
|
+
lines.push(`Note: ${warning.message}`);
|
|
59
|
+
}
|
|
55
60
|
lines.push('');
|
|
56
61
|
|
|
57
62
|
// Before/After
|
|
@@ -63,6 +68,13 @@ function formatPlan(plan, options = {}) {
|
|
|
63
68
|
// Summary
|
|
64
69
|
lines.push(`CHANGES NEEDED: ${plan.totalChanges}`);
|
|
65
70
|
lines.push(` Files affected: ${plan.filesAffected}`);
|
|
71
|
+
if (plan.changeSummary) {
|
|
72
|
+
const summary = plan.changeSummary;
|
|
73
|
+
lines.push(` Definition ${summary.definitions}, calls/references ${summary.calls}, imports ${summary.imports}, exports ${summary.exports}; manual review required for ${summary.reviewRequired} of these changes`);
|
|
74
|
+
}
|
|
75
|
+
if (plan.unchangedSites > 0) {
|
|
76
|
+
lines.push(` ${plan.unchangedSites} existing call site${plan.unchangedSites === 1 ? '' : 's'} require no edit because the new parameter has a default.`);
|
|
77
|
+
}
|
|
66
78
|
if (plan.scopeWarning) {
|
|
67
79
|
lines.push(` Note: ${plan.scopeWarning.hint}`);
|
|
68
80
|
}
|
|
@@ -79,9 +91,11 @@ function formatPlan(plan, options = {}) {
|
|
|
79
91
|
|
|
80
92
|
lines.push('BY FILE:');
|
|
81
93
|
for (const [file, changes] of byFile) {
|
|
82
|
-
lines.push(`\n${file} (${changes.length} changes)`);
|
|
94
|
+
lines.push(`\n${file} (${changes.length} change${changes.length === 1 ? '' : 's'})`);
|
|
83
95
|
for (const change of changes) {
|
|
84
|
-
|
|
96
|
+
const kind = change.editKind ? ` [${change.editKind}]` : '';
|
|
97
|
+
const review = change.needsReview ? ' [review required]' : '';
|
|
98
|
+
lines.push(` :${change.line}${kind}${review}`);
|
|
85
99
|
lines.push(` ${change.expression}`);
|
|
86
100
|
lines.push(` → ${change.suggestion}`);
|
|
87
101
|
}
|
|
@@ -128,6 +142,7 @@ function formatPlanJson(plan) {
|
|
|
128
142
|
complete: (plan.unverifiedCount || 0) === 0,
|
|
129
143
|
unverified: plan.unverifiedCount || 0,
|
|
130
144
|
...(plan.account && { account: plan.account }),
|
|
145
|
+
...(plan.warnings?.length > 0 && { warnings: plan.warnings }),
|
|
131
146
|
},
|
|
132
147
|
data: {
|
|
133
148
|
found: true,
|
|
@@ -139,11 +154,19 @@ function formatPlanJson(plan) {
|
|
|
139
154
|
after: { signature: plan.after.signature },
|
|
140
155
|
totalChanges: plan.totalChanges,
|
|
141
156
|
filesAffected: plan.filesAffected,
|
|
157
|
+
...(plan.changeSummary && { changeSummary: plan.changeSummary }),
|
|
158
|
+
...(plan.unchangedSites > 0 && { unchangedSites: plan.unchangedSites }),
|
|
142
159
|
changes: plan.changes.map(c => ({
|
|
143
160
|
file: c.file,
|
|
144
161
|
line: c.line,
|
|
145
162
|
expression: c.expression,
|
|
146
|
-
suggestion: c.suggestion
|
|
163
|
+
suggestion: c.suggestion,
|
|
164
|
+
...(c.newExpression && { newExpression: c.newExpression }),
|
|
165
|
+
...(c.editKind && { editKind: c.editKind }),
|
|
166
|
+
...(c.isDefinition && { isDefinition: true }),
|
|
167
|
+
...(c.isImport && { isImport: true }),
|
|
168
|
+
...(c.isExport && { isExport: true }),
|
|
169
|
+
...(c.needsReview && { needsReview: true }),
|
|
147
170
|
})),
|
|
148
171
|
// v4 tiered contract passthrough
|
|
149
172
|
unverifiedCount: plan.unverifiedCount,
|
|
@@ -187,6 +210,9 @@ function formatVerify(result, options = {}) {
|
|
|
187
210
|
lines.push('═'.repeat(60));
|
|
188
211
|
lines.push(`${result.file}:${result.startLine}`);
|
|
189
212
|
lines.push(result.signature);
|
|
213
|
+
for (const warning of result.warnings || []) {
|
|
214
|
+
lines.push(`Note: ${warning.message}`);
|
|
215
|
+
}
|
|
190
216
|
lines.push('');
|
|
191
217
|
|
|
192
218
|
// Expected args (max null = unbounded rest param)
|
|
@@ -196,16 +222,21 @@ function formatVerify(result, options = {}) {
|
|
|
196
222
|
lines.push('');
|
|
197
223
|
|
|
198
224
|
// Summary
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// valid === 0 && uncertain > 0 → all-uncertain; valid > 0 && mismatches === 0 → ok.
|
|
225
|
+
// A partial check is never a clean pass. Agents often consume the headline
|
|
226
|
+
// and omit the counters, so any uncertain site must remain visible there.
|
|
202
227
|
let status;
|
|
203
228
|
if (result.mismatches > 0) {
|
|
204
|
-
status = `✗ ${result.mismatches} mismatch${result.mismatches === 1 ? '' : 'es'}
|
|
229
|
+
status = `✗ ${result.mismatches} mismatch${result.mismatches === 1 ? '' : 'es'}` +
|
|
230
|
+
(result.uncertain > 0 ? `; ${result.uncertain} uncertain` : '');
|
|
205
231
|
} else if (result.totalCalls === 0) {
|
|
206
232
|
status = 'ℹ No calls found';
|
|
207
233
|
} else if (result.valid === 0 && result.uncertain > 0) {
|
|
208
234
|
status = '⚠ All calls uncertain (no resolved sites)';
|
|
235
|
+
} else if (result.uncertain > 0) {
|
|
236
|
+
status = `⚠ Partial verification: ${result.valid} valid, ${result.uncertain} uncertain`;
|
|
237
|
+
} else if (result.unverifiedCount > 0) {
|
|
238
|
+
status = `⚠ ${result.valid} confirmed call${result.valid === 1 ? '' : 's'} valid; ` +
|
|
239
|
+
`${result.unverifiedCount} unverified site${result.unverifiedCount === 1 ? '' : 's'} not arg-checked`;
|
|
209
240
|
} else {
|
|
210
241
|
status = '✓ All calls valid';
|
|
211
242
|
}
|
|
@@ -291,6 +322,7 @@ function formatVerifyJson(result) {
|
|
|
291
322
|
uncertain: result.uncertain,
|
|
292
323
|
unverified: result.unverifiedCount || 0,
|
|
293
324
|
...(result.account && { account: result.account }),
|
|
325
|
+
...(result.warnings?.length > 0 && { warnings: result.warnings }),
|
|
294
326
|
},
|
|
295
327
|
data: {
|
|
296
328
|
found: true,
|
|
@@ -343,7 +375,7 @@ function formatStackTrace(result) {
|
|
|
343
375
|
const stAdvisory = advisoryLine(result.advisory);
|
|
344
376
|
if (stAdvisory) lines.push(stAdvisory);
|
|
345
377
|
if (result.skippedFrames > 0) {
|
|
346
|
-
lines.push(`(${result.skippedFrames} frame(s) without
|
|
378
|
+
lines.push(`(${result.skippedFrames} runtime frame(s) outside the indexed project or without a resolvable project file — skipped)`);
|
|
347
379
|
}
|
|
348
380
|
|
|
349
381
|
for (let i = 0; i < result.frames.length; i++) {
|