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.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. 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: ${plan.error}\nCurrent parameters: ${plan.currentParams.join(', ') || 'none'}`
46
- : `Error: ${plan.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
- lines.push(` :${change.line}`);
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
- // BUG M1: don't claim "All calls valid" when 0 valid + N uncertain.
200
- // Status precedence: mismatches > 0 → fail; total === 0 → empty;
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 file:line — Unknown Source, native — skipped)`);
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++) {