ucn 4.2.2 → 5.0.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.
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 +445 -300
  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 -131
  13. package/core/cache.js +533 -11
  14. package/core/callers.js +5533 -494
  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 +421 -20
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +204 -42
  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 +216 -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 -177
  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 +371 -116
  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 +428 -16
  65. package/languages/javascript.js +452 -49
  66. package/languages/python.js +1041 -32
  67. package/languages/rust.js +1415 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +41 -24
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -88,17 +88,34 @@ function formatAccountLines(account) {
88
88
  `${account.confirmed} confirmed, ${account.unverified} unverified, ` +
89
89
  `${nc.total} non-call (${nc.imports} import, ${nc.definitions} definition, ${nc.references} reference, ${nc.unclassifiedText} other-text), ` +
90
90
  `${account.excluded ? account.excluded.total : 0} other-target, ` +
91
+ `${account.unsupported && account.unsupported.lines > 0 ? `${account.unsupported.lines} unsupported-language, ` : ''}` +
91
92
  `${account.unaccounted} unaccounted`;
92
93
  if (account.beyondText && account.beyondText.count > 0) {
93
94
  line += ` (+${account.beyondText.count} beyond-text caller${account.beyondText.count === 1 ? '' : 's'})`;
94
95
  }
95
96
  lines.push(line);
96
97
  const contract = account.contract || {};
98
+ const unsupported = account.unsupported ||
99
+ { fileCount: 0, lines: 0, files: [], languages: {}, sites: [], sitesTruncated: false };
97
100
  const textComplete = contract.textComplete !== undefined
98
101
  ? contract.textComplete
99
102
  : Boolean(account.conserved) && !(account.unparsed && account.unparsed.fileCount > 0) &&
103
+ unsupported.lines === 0 &&
100
104
  !(account.unreadableFiles && account.unreadableFiles.length > 0);
101
- if (!textComplete) {
105
+ // When the ONLY gap is unsupported-language occurrences, say exactly that
106
+ // instead of the generic DEGRADED sentence: the partition over supported
107
+ // languages is still conserved, and the gap is enumerated below.
108
+ const unsupportedOnly = !textComplete && unsupported.lines > 0 &&
109
+ Boolean(account.conserved) &&
110
+ !(account.unparsed && account.unparsed.fileCount > 0) &&
111
+ !(account.unreadableFiles && account.unreadableFiles.length > 0);
112
+ if (unsupportedOnly) {
113
+ const langs = Object.keys(unsupported.languages).join(', ');
114
+ lines.push(`CONTRACT: literal-name text partition complete over SUPPORTED languages only — ` +
115
+ `${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in unsupported-language file${unsupported.fileCount === 1 ? '' : 's'}` +
116
+ `${langs ? ` (${langs})` : ''} NOT analyzed (see WARNING; verify with grep/ripgrep). ` +
117
+ 'Semantic completeness is not claimed.');
118
+ } else if (!textComplete) {
102
119
  lines.push('CONTRACT: literal-name text partition is DEGRADED; review WARNING and unaccounted lines before acting. Semantic completeness is not claimed.');
103
120
  } else if (contract.observedTextZero) {
104
121
  lines.push('CONTRACT: observed-text zero only; every literal-name line was classified, but aliases, indirect calls, generated code, and runtime dispatch may exist. Not safe-delete proof.');
@@ -111,8 +128,31 @@ function formatAccountLines(account) {
111
128
  `(${account.unparsed.lines} line${account.unparsed.lines === 1 ? '' : 's'}, NOT analyzed): ` +
112
129
  account.unparsed.files.join(', '));
113
130
  }
131
+ if (unsupported.lines > 0) {
132
+ const langParts = Object.entries(unsupported.languages)
133
+ .map(([lang, count]) => `${lang}: ${count}`).join(', ');
134
+ lines.push(`WARNING: ${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in ` +
135
+ `${unsupported.fileCount} unsupported-language file${unsupported.fileCount === 1 ? '' : 's'} ` +
136
+ `contain${unsupported.lines === 1 ? 's' : ''} "${account.symbol}"` +
137
+ `${langParts ? ` (${langParts})` : ''} — NOT analyzed; verify with grep/ripgrep:`);
138
+ const shown = unsupported.sites.slice(0, 5);
139
+ for (const site of shown) {
140
+ lines.push(` ${site.file}:${site.line} ${site.text}`);
141
+ }
142
+ if (unsupported.lines > shown.length) {
143
+ lines.push(` ... and ${unsupported.lines - shown.length} more line${unsupported.lines - shown.length === 1 ? '' : 's'}`);
144
+ }
145
+ }
114
146
  if (account.unreadableFiles && account.unreadableFiles.length > 0) {
115
- lines.push(`WARNING: ${account.unreadableFiles.length} indexed-but-unreadable file(s) skipped: ${account.unreadableFiles.join(', ')}`);
147
+ lines.push(`WARNING: ${account.unreadableFiles.length} unreadable file(s) skipped: ${account.unreadableFiles.join(', ')}`);
148
+ }
149
+ if (account.skippedSources && account.skippedSources.length > 0) {
150
+ const shown = account.skippedSources.slice(0, 5)
151
+ .map(issue => `${issue.relativePath} (${issue.reason})`);
152
+ lines.push(`WARNING: ${account.skippedSources.length} source discovery gap(s) make this index partial: ${shown.join(', ')}`);
153
+ if (account.skippedSources.length > shown.length) {
154
+ lines.push(` ... and ${account.skippedSources.length - shown.length} more; run repo --sections=health for the full reason set.`);
155
+ }
116
156
  }
117
157
  if (account.filtered && account.filtered.total > 0) {
118
158
  const parts = [];
@@ -156,11 +196,18 @@ function unverifiedCalleeLines(entries, compact) {
156
196
  }
157
197
 
158
198
  /** "NON-CALL OCCURRENCES" summary line from the account. */
159
- function formatNonCallLine(account, hintName) {
199
+ function formatNonCallLine(account, hintName, usagesHint) {
160
200
  if (!account || !account.nonCall || account.nonCall.total === 0) return null;
161
201
  const nc = account.nonCall;
202
+ const hint = usagesHint || `ucn usages ${hintName}`;
203
+ // Classification differs across the two surfaces by contract: these
204
+ // counts are engine-adjudicated (a method reference that resolves to
205
+ // this target is a confirmed caller, not a reference), while `usages`
206
+ // classifies each line by syntax alone — the breakdowns can
207
+ // legitimately differ.
162
208
  return `NON-CALL OCCURRENCES: ${nc.total} (${nc.imports} imports, ${nc.definitions} definitions, ` +
163
- `${nc.references} references, ${nc.unclassifiedText} other-text) — counts only; see: ucn usages ${hintName}`;
209
+ `${nc.references} references, ${nc.unclassifiedText} other-text) — engine-adjudicated counts; ` +
210
+ `syntax-classified line records: ${hint}`;
164
211
  }
165
212
 
166
213
  /** Format context (callers + callees) as JSON */
@@ -191,8 +238,11 @@ function formatContextJson(context) {
191
238
  usages: callers.map(c => ({
192
239
  file: c.relativePath || c.file,
193
240
  line: c.line,
241
+ ...(Number.isInteger(c.column) && { column: c.column }),
194
242
  expression: c.content,
195
243
  callerName: c.callerName,
244
+ ...(c.calledAs && { calledAs: c.calledAs }),
245
+ ...(c.isFunctionReference && { functionReference: true }),
196
246
  // Tier parity with the function-path callers list: class
197
247
  // usages are the confirmed-tier answer for type symbols.
198
248
  ...(c.confidence !== undefined && { confidence: c.confidence }),
@@ -204,8 +254,11 @@ function formatContextJson(context) {
204
254
  unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
205
255
  file: c.relativePath || c.file,
206
256
  line: c.line,
257
+ ...(Number.isInteger(c.column) && { column: c.column }),
207
258
  expression: c.content,
208
259
  callerName: c.callerName ?? null,
260
+ ...(c.calledAs && { calledAs: c.calledAs }),
261
+ ...(c.isFunctionReference && { functionReference: true }),
209
262
  tier: 'unverified',
210
263
  ...(c.confidence !== undefined && { confidence: c.confidence }),
211
264
  ...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
@@ -215,6 +268,10 @@ function formatContextJson(context) {
215
268
  ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
216
269
  ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
217
270
  ...(c.externalContract && { externalContract: true }),
271
+ uncertaintyClass: c.uncertaintyClass ||
272
+ (c.reason === 'possible-dispatch' && c.dispatchVia
273
+ ? 'runtime-dispatch' : 'actionable-ambiguity'),
274
+ ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
218
275
  })),
219
276
  ...(context.warnings && { warnings: context.warnings })
220
277
  }
@@ -238,6 +295,7 @@ function formatContextJson(context) {
238
295
  callers: callers.map(c => ({
239
296
  file: c.relativePath || c.file,
240
297
  line: c.line,
298
+ ...(Number.isInteger(c.column) && { column: c.column }),
241
299
  expression: c.content, // FULL expression
242
300
  callerName: c.callerName,
243
301
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -251,6 +309,7 @@ function formatContextJson(context) {
251
309
  unverifiedCallers: unverifiedCallers.map(c => ({
252
310
  file: c.relativePath || c.file,
253
311
  line: c.line,
312
+ ...(Number.isInteger(c.column) && { column: c.column }),
254
313
  expression: c.content, // FULL expression
255
314
  callerName: c.callerName ?? null,
256
315
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -263,6 +322,10 @@ function formatContextJson(context) {
263
322
  ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
264
323
  ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
265
324
  ...(c.externalContract && { externalContract: true }),
325
+ uncertaintyClass: c.uncertaintyClass ||
326
+ (c.reason === 'possible-dispatch' && c.dispatchVia
327
+ ? 'runtime-dispatch' : 'actionable-ambiguity'),
328
+ ...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
266
329
  })),
267
330
  callees: callees.map(c => ({
268
331
  name: c.name,
@@ -295,13 +358,14 @@ function formatContext(ctx, options = {}) {
295
358
  if (!ctx) return { text: 'Symbol not found.', expandable: [] };
296
359
 
297
360
  const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
361
+ const allHint = options.allHint || 'use --all';
298
362
 
299
363
  const lines = [];
300
364
  const expandable = [];
301
365
  let itemNum = 1;
302
366
 
303
367
  // Handle struct/interface types
304
- if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
368
+ if (ctx.type && ['class', 'struct', 'interface', 'type', 'enum', 'record', 'trait', 'namespace'].includes(ctx.type)) {
305
369
  lines.push(`Context for ${ctx.type} ${ctx.name}:`);
306
370
  lines.push('═'.repeat(60));
307
371
 
@@ -330,8 +394,20 @@ function formatContext(ctx, options = {}) {
330
394
  });
331
395
  }
332
396
 
397
+ const members = ctx.members || [];
398
+ if (members.length > 0) {
399
+ lines.push(`\nMEMBERS (${members.length}):`);
400
+ for (const member of members) {
401
+ lines.push(` ${member.name} (${member.type})`);
402
+ lines.push(` ${member.file}:${member.line}`);
403
+ }
404
+ }
405
+
333
406
  const callers = ctx.callers || [];
334
- lines.push(`\nCALLERS — CONFIRMED (${callers.length}):`);
407
+ const typeCallerTotal = ctx.meta?.callerTotal ?? callers.length;
408
+ const typeCallerLabel = typeCallerTotal > callers.length
409
+ ? `${callers.length} shown of ${typeCallerTotal}` : `${callers.length}`;
410
+ lines.push(`\nCALLERS — CONFIRMED (${typeCallerLabel}):`);
335
411
  for (const c of callers) {
336
412
  const callerName = c.callerName ? ` [${c.callerName}]` : '';
337
413
  lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
@@ -372,7 +448,7 @@ function formatContext(ctx, options = {}) {
372
448
  shown++;
373
449
  }
374
450
  if (typeUnverified.length > shown) {
375
- lines.push(` (+${typeUnverified.length - shown} more unverified — use --all)`);
451
+ lines.push(` (+${typeUnverified.length - shown} more unverified — ${allHint})`);
376
452
  }
377
453
  }
378
454
 
@@ -420,9 +496,12 @@ function formatContext(ctx, options = {}) {
420
496
  const callers = ctx.callers || [];
421
497
  const prodCallers = callers.filter(c => !isTestEntry(c));
422
498
  const testCallers = callers.filter(c => isTestEntry(c));
423
- const tierHeader = testCallers.length > 0
424
- ? `CALLERS — CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
425
- : `CALLERS — CONFIRMED (${callers.length}):`;
499
+ const callerTotal = ctx.meta?.callerTotal ?? callers.length;
500
+ const tierHeader = callerTotal > callers.length
501
+ ? `CALLERS — CONFIRMED (${callers.length} shown of ${callerTotal}):`
502
+ : testCallers.length > 0
503
+ ? `CALLERS — CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
504
+ : `CALLERS — CONFIRMED (${callers.length}):`;
426
505
  lines.push(`${compact ? '' : '\n'}${tierHeader}`);
427
506
  const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
428
507
  if (callerEvidence) lines.push(callerEvidence);
@@ -460,15 +539,123 @@ function formatContext(ctx, options = {}) {
460
539
  // candidates ARE analyzed — they render in the unverified band with
461
540
  // reasons, and the ACCOUNT line reconciles every text occurrence.
462
541
 
463
- // UNVERIFIED tier: call-syntax matches without binding/receiver evidence.
464
- // Always visible (the contract: never silently hide an occurrence), capped
465
- // at 10 one-liners unless --all.
542
+ // Runtime dispatch has a known boundary (trait/interface/supertype or a
543
+ // capability guard), but the concrete implementation is selected at
544
+ // runtime. Present repeated sites as one decision family with samples:
545
+ // the raw sites remain in JSON/accounting and --all expands every one.
546
+ // This separates unavoidable language semantics from ambiguity the agent
547
+ // can investigate.
466
548
  const unverified = ctx.unverifiedCallers || [];
467
- if (unverified.length > 0) {
468
- lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${unverified.length}) — call syntax, no binding/receiver evidence:`);
549
+ const runtimeDispatch = unverified.filter(u =>
550
+ u.uncertaintyClass === 'runtime-dispatch' ||
551
+ (u.reason === 'possible-dispatch' && u.dispatchVia));
552
+ const compileTimeDispatch = unverified.filter(u =>
553
+ u.uncertaintyClass === 'compile-time-dispatch');
554
+ const actionableUnverified = unverified.filter(u =>
555
+ u.uncertaintyClass !== 'compile-time-dispatch' &&
556
+ !(u.uncertaintyClass === 'runtime-dispatch' ||
557
+ (u.reason === 'possible-dispatch' && u.dispatchVia)));
558
+ if (runtimeDispatch.length > 0) {
559
+ const groups = new Map();
560
+ for (const site of runtimeDispatch) {
561
+ const key = `${site.dispatchVia}\0${site.dispatchCandidates ?? ''}\0` +
562
+ `${site.externalContract ? 'external' : 'project'}`;
563
+ if (!groups.has(key)) groups.set(key, []);
564
+ groups.get(key).push(site);
565
+ }
566
+ lines.push(`${compact ? '' : '\n'}CALLERS — RUNTIME DISPATCH ` +
567
+ `(${runtimeDispatch.length} site${runtimeDispatch.length === 1 ? '' : 's'}, ` +
568
+ `${groups.size} ${groups.size === 1 ? 'family' : 'families'}) — ` +
569
+ 'implementation selected at runtime:');
570
+ const showAll = !!ctx.meta?.all;
571
+ let summarized = 0;
572
+ for (const sites of groups.values()) {
573
+ const first = sites[0];
574
+ const implementation = first.dispatchCandidates > 1
575
+ ? `; 1 of ${first.dispatchCandidates} implementations`
576
+ : first.externalContract ? '; open external implementation set' : '';
577
+ lines.push(` via ${first.dispatchVia}: ${sites.length} ` +
578
+ `site${sites.length === 1 ? '' : 's'}${implementation}`);
579
+ const samples = showAll ? sites : sites.slice(0, 2);
580
+ for (const u of samples) {
581
+ const callerName = u.callerName ? ` [${u.callerName}]` : '';
582
+ const expr = u.content
583
+ ? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
584
+ lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}`);
585
+ expandable.push({
586
+ num: itemNum++,
587
+ type: 'caller',
588
+ name: u.callerName || '(module level)',
589
+ file: u.callerFile || u.file,
590
+ relativePath: u.relativePath,
591
+ line: u.line,
592
+ startLine: u.callerStartLine || u.line,
593
+ endLine: u.callerEndLine || u.line
594
+ });
595
+ }
596
+ summarized += sites.length - samples.length;
597
+ }
598
+ if (summarized > 0) {
599
+ lines.push(` (+${summarized} more runtime-dispatch sites — ${allHint})`);
600
+ }
601
+ }
602
+
603
+ // Template substitution, constraints, and dependent calls are selected by
604
+ // the C++ compiler. Repeating every source site as generic actionable
605
+ // ambiguity makes an agent inspect code it cannot settle statically. Keep
606
+ // every raw site in JSON/accounting and group the text view by the exact
607
+ // overload family that needs a compiler handoff.
608
+ if (compileTimeDispatch.length > 0) {
609
+ const groups = new Map();
610
+ for (const site of compileTimeDispatch) {
611
+ const key = `${site.dispatchFamily || 'template overload set'}\0` +
612
+ `${site.dispatchCandidates ?? ''}`;
613
+ if (!groups.has(key)) groups.set(key, []);
614
+ groups.get(key).push(site);
615
+ }
616
+ lines.push(`${compact ? '' : '\n'}CALLERS — COMPILE-TIME DISPATCH ` +
617
+ `(${compileTimeDispatch.length} site${compileTimeDispatch.length === 1 ? '' : 's'}, ` +
618
+ `${groups.size} ${groups.size === 1 ? 'family' : 'families'}) — ` +
619
+ 'exact overload depends on template substitution or constraints:');
620
+ const showAll = !!ctx.meta?.all;
621
+ let summarized = 0;
622
+ for (const sites of groups.values()) {
623
+ const first = sites[0];
624
+ const candidates = first.dispatchCandidates > 1
625
+ ? `; ${first.dispatchCandidates} candidate overloads` : '';
626
+ lines.push(` ${first.dispatchFamily || 'template overload set'}: ` +
627
+ `${sites.length} site${sites.length === 1 ? '' : 's'}${candidates}`);
628
+ const samples = showAll ? sites : sites.slice(0, 2);
629
+ for (const u of samples) {
630
+ const callerName = u.callerName ? ` [${u.callerName}]` : '';
631
+ const expr = u.content
632
+ ? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
633
+ lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}`);
634
+ expandable.push({
635
+ num: itemNum++,
636
+ type: 'caller',
637
+ name: u.callerName || '(module level)',
638
+ file: u.callerFile || u.file,
639
+ relativePath: u.relativePath,
640
+ line: u.line,
641
+ startLine: u.callerStartLine || u.line,
642
+ endLine: u.callerEndLine || u.line
643
+ });
644
+ }
645
+ summarized += sites.length - samples.length;
646
+ }
647
+ if (summarized > 0) {
648
+ lines.push(` (+${summarized} more compile-time-dispatch sites — ${allHint})`);
649
+ }
650
+ }
651
+
652
+ // Actionable ambiguity: call syntax without enough identity evidence.
653
+ // Always visible and capped at 10 one-liners unless --all.
654
+ if (actionableUnverified.length > 0) {
655
+ lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
469
656
  const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
470
657
  let shown = 0;
471
- for (const u of unverified) {
658
+ for (const u of actionableUnverified) {
472
659
  if (shown >= cap) break;
473
660
  const callerName = u.callerName ? ` [${u.callerName}]` : '';
474
661
  const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
@@ -486,13 +673,16 @@ function formatContext(ctx, options = {}) {
486
673
  });
487
674
  shown++;
488
675
  }
489
- if (unverified.length > shown) {
490
- lines.push(` (+${unverified.length - shown} more unverified — use --all)`);
676
+ if (actionableUnverified.length > shown) {
677
+ lines.push(` (+${actionableUnverified.length - shown} more unverified — ${allHint})`);
491
678
  }
492
679
  }
493
680
 
494
681
  const callees = ctx.callees || [];
495
- lines.push(`${compact ? '' : '\n'}CALLEES (${callees.length}):`);
682
+ const calleeTotal = ctx.meta?.calleeTotal ?? callees.length;
683
+ const calleeLabel = calleeTotal > callees.length
684
+ ? `${callees.length} shown of ${calleeTotal}` : `${callees.length}`;
685
+ lines.push(`${compact ? '' : '\n'}CALLEES (${calleeLabel}):`);
496
686
  const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
497
687
  if (calleeEvidence) lines.push(calleeEvidence);
498
688
  const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
@@ -531,7 +721,7 @@ function formatContext(ctx, options = {}) {
531
721
  // Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
532
722
  const account = ctx.meta && ctx.meta.account;
533
723
  if (account) {
534
- const nonCallLine = formatNonCallLine(account, ctx.function);
724
+ const nonCallLine = formatNonCallLine(account, ctx.function, options.usagesHint);
535
725
  if (nonCallLine) lines.push(`${compact ? '' : '\n'}${nonCallLine}`);
536
726
  const accountLines = formatAccountLines(account);
537
727
  if (accountLines.length > 0) {
@@ -563,6 +753,9 @@ function formatImpact(impact, options = {}) {
563
753
  if (!compact) lines.push('═'.repeat(60));
564
754
  lines.push(`${impact.file}:${impact.startLine}`);
565
755
  if (!compact) lines.push(impact.signature);
756
+ for (const warning of impact.warnings || []) {
757
+ lines.push(`Note: ${warning.message}`);
758
+ }
566
759
  if (!compact) lines.push('');
567
760
 
568
761
  // Summary (confirmed + unverified tiers reported separately)
@@ -689,6 +882,7 @@ function formatAbout(about, options = {}) {
689
882
  }
690
883
 
691
884
  const lines = [];
885
+ const allHint = options.allHint || 'use --all';
692
886
  const sym = about.symbol;
693
887
  const { expand, root, depth } = options;
694
888
 
@@ -805,7 +999,7 @@ function formatAbout(about, options = {}) {
805
999
  lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
806
1000
  }
807
1001
  if (aboutUnverified.total > aboutUnverified.top.length) {
808
- lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — use --all)`);
1002
+ lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — ${allHint})`);
809
1003
  }
810
1004
  }
811
1005
 
@@ -923,7 +1117,7 @@ function formatAbout(about, options = {}) {
923
1117
  lines.push(about.code);
924
1118
  } else if (about.code && compact) {
925
1119
  lines.push('');
926
- lines.push(`SOURCE: omitted in compact mode; use fn ${sym.handle || sym.name} to extract it.`);
1120
+ lines.push(`SOURCE: omitted in compact mode; use source ${sym.handle || sym.name} to extract it.`);
927
1121
  }
928
1122
 
929
1123
  if (aboutTruncated) {
@@ -14,6 +14,18 @@ function signatureLine(sym) {
14
14
  const parts = [];
15
15
  if (sym.modifiers && sym.modifiers.length) parts.push(sym.modifiers.join(' '));
16
16
  let sig = sym.name;
17
+ if (sym.generics) sig += String(sym.generics).replace(/\s+/g, ' ').trim();
18
+ if (sym.type === 'field' || sym.type === 'state' ||
19
+ sym.memberType === 'field' || sym.memberType === 'property') {
20
+ const declaredType = sym.fieldType || sym.returnType;
21
+ if (declaredType) sig += `: ${String(declaredType).replace(/\s+/g, ' ').trim()}`;
22
+ parts.push(sig);
23
+ return parts.join(' ');
24
+ }
25
+ if (sym.type === 'macro' && sym.functionLike === false) {
26
+ parts.push(sig);
27
+ return parts.join(' ');
28
+ }
17
29
  const typed = renderTypedParams(sym);
18
30
  // If we have a structured-params array of length 0, the function has no params.
19
31
  // Render `()` rather than the legacy `(...)` placeholder.
@@ -57,6 +69,17 @@ function formatBrief(result) {
57
69
  return lines.join('\n');
58
70
  }
59
71
 
72
+ if (result.kind === 'data') {
73
+ lines.push(signatureLine(sym));
74
+ lines.push(` ${sym.file}:${sym.startLine}-${sym.endLine} (${result.lineCount || 0} line${result.lineCount === 1 ? '' : 's'})`);
75
+ if (sym.handle) lines.push(` handle: ${sym.handle}`);
76
+ if (sym.docstring) lines.push(` "${sym.docstring}"`);
77
+ if (sym.className) lines.push(` in class ${sym.className}`);
78
+ const gitLineData = formatGitLine(result.git);
79
+ if (gitLineData) lines.push(` ${gitLineData}`);
80
+ return lines.join('\n');
81
+ }
82
+
60
83
  // Header line: signature
61
84
  lines.push(signatureLine(sym));
62
85
  // Location + line count
@@ -6,6 +6,10 @@
6
6
 
7
7
  function formatCheck(result) {
8
8
  if (!result) return 'No check result.';
9
+ if (result.ok === false) {
10
+ // The gate could not run — never render this like a clean tree.
11
+ 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'}\nThis is not a pass. Fix the git context (run inside a git repository with a valid base ref) and rerun.`;
12
+ }
9
13
  if (result.empty) {
10
14
  return `Pre-commit Check (${result.base}${result.staged ? ', staged' : ''})\n${'═'.repeat(60)}\nNo changes to analyze${result.reason ? ` (${result.reason})` : ''}.`;
11
15
  }
@@ -4,7 +4,9 @@
4
4
 
5
5
  'use strict';
6
6
 
7
- function formatDoctor(result) {
7
+ const { langTraits } = require('../../languages');
8
+
9
+ function formatDoctor(result, options = {}) {
8
10
  if (!result) return 'No project to analyze.';
9
11
  const lines = [];
10
12
  lines.push(`UCN Trust Report — ${result.root}`);
@@ -12,14 +14,20 @@ function formatDoctor(result) {
12
14
  if (result.version) lines.push(`Version: ucn ${result.version}`);
13
15
  lines.push(`Index: ${result.files.scanned} file${result.files.scanned === 1 ? '' : 's'}, ${result.symbols} symbol${result.symbols === 1 ? '' : 's'}`);
14
16
 
15
- if (result.filter) lines.push(`Filter: ${result.filter}`);
17
+ if (result.filter) {
18
+ const parts = [];
19
+ if (result.filter.file) parts.push(`file=${result.filter.file}`);
20
+ if (result.filter.in) parts.push(`in=${result.filter.in}`);
21
+ if (result.filter.exclude?.length) parts.push(`exclude=${result.filter.exclude.join(',')}`);
22
+ if (parts.length > 0) lines.push(`Filter: ${parts.join(' · ')}`);
23
+ }
16
24
 
17
25
  // Languages
18
26
  const langEntries = Object.entries(result.languages || {}).sort((a, b) => b[1].files - a[1].files);
19
27
  if (langEntries.length) {
20
28
  const totalFiles = langEntries.reduce((s, [, v]) => s + v.files, 0) || 1;
21
29
  const langStr = langEntries.map(([name, v]) => `${name} (${Math.round(v.files / totalFiles * 100)}%)`).join(', ');
22
- lines.push(`Languages: ${langStr}`);
30
+ lines.push(`Languages (by indexed files): ${langStr}`);
23
31
  }
24
32
 
25
33
  // Cache state
@@ -52,23 +60,35 @@ function formatDoctor(result) {
52
60
  lines.push('Resolution evidence profile: no caller edges in the stratified sample.');
53
61
  } else {
54
62
  lines.push('');
55
- lines.push('Resolution evidence profile: not computed (use --deep)');
63
+ lines.push(`Resolution evidence profile: not computed (${options.deepHint || 'use --deep'})`);
56
64
  }
57
65
 
58
66
  // Blind spots
59
67
  lines.push('');
60
68
  lines.push('Blind spots:');
61
69
  const bs = result.blindSpots || {};
70
+ const staticSpecialImports = result.projectLanguage &&
71
+ !langTraits(result.projectLanguage)?.hasDynamicImports;
72
+ const importLabel = staticSpecialImports
73
+ ? (result.projectLanguage === 'rust' ? 'Glob imports' : 'Blank/dot imports')
74
+ : 'Dynamic imports';
62
75
  const bsLines = [
63
- ['Dynamic imports', bs.dynamicImports],
76
+ [importLabel, bs.dynamicImports],
64
77
  ['Eval/exec calls', bs.evalCalls],
65
78
  ['Reflection', bs.reflection],
79
+ ['Computed dispatch', bs.computedDispatch],
66
80
  ['Parse failures', bs.parseFailures],
67
81
  ['Parser recovery', bs.parseRecoveries],
82
+ ['Unsupported source', bs.unsupportedSources],
83
+ ['Skipped source', bs.skippedSources],
68
84
  ];
69
85
  const unitFor = {
70
- 'Dynamic imports': 'import', 'Eval/exec calls': 'use', Reflection: 'use',
86
+ 'Dynamic imports': 'import', 'Blank/dot imports': 'import',
87
+ 'Glob imports': 'import', 'Eval/exec calls': 'use', Reflection: 'use',
88
+ 'Computed dispatch': 'call',
71
89
  'Parse failures': 'failure',
90
+ 'Unsupported source': 'file',
91
+ 'Skipped source': 'path',
72
92
  };
73
93
  let anyBlindSpot = false;
74
94
  for (const [label, info] of bsLines) {
@@ -82,6 +102,17 @@ function formatDoctor(result) {
82
102
  const unit = unitFor[label] || 'use';
83
103
  if (label === 'Parser recovery') {
84
104
  lines.push(` ${label}: ${fileCount} recovered file${fileCount === 1 ? '' : 's'} (results may be partial)`);
105
+ } else if (label === 'Unsupported source') {
106
+ const languages = Object.entries(info.languages || {})
107
+ .map(([language, count]) => `${language} ${count}`)
108
+ .join(', ');
109
+ lines.push(` ${label}: ${fileCount} file${fileCount === 1 ? '' : 's'} skipped${languages ? ` (${languages})` : ''}`);
110
+ lines.push(' Handoff: use grep/ripgrep and a language-native analyzer for these files.');
111
+ } else if (label === 'Skipped source') {
112
+ const reasons = Object.entries(info.reasons || {})
113
+ .map(([reason, count]) => `${reason} ${count}`).join(', ');
114
+ lines.push(` ${label}: ${fileCount} path${fileCount === 1 ? '' : 's'} not indexed${reasons ? ` (${reasons})` : ''}`);
115
+ lines.push(' The index is partial; adjust .ucn.json or discovery limits before relying on completeness-sensitive commands.');
85
116
  } else {
86
117
  lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
87
118
  }
@@ -26,7 +26,7 @@ function formatEndpoints(result, options = {}) {
26
26
  const showBridge = options.bridge;
27
27
 
28
28
  if (!showBridge) {
29
- return formatRoutesAndRequests(routes, requests, meta, options);
29
+ return formatRoutesAndRequests(routes, requests, meta, options, result.advisory);
30
30
  }
31
31
  return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options, result.advisory);
32
32
  }
@@ -48,7 +48,7 @@ function uniqueMatchPercent(bridges, totalRequests) {
48
48
  return Math.min(100, Math.max(0, pct));
49
49
  }
50
50
 
51
- function formatRoutesAndRequests(routes, requests, meta, options) {
51
+ function formatRoutesAndRequests(routes, requests, meta, options, advisory = null) {
52
52
  const lines = [];
53
53
  const showServer = !options.clientOnly;
54
54
  const showClient = !options.serverOnly;
@@ -109,6 +109,8 @@ function formatRoutesAndRequests(routes, requests, meta, options) {
109
109
  }
110
110
  }
111
111
 
112
+ const routeAdvisory = advisoryLine(advisory);
113
+ if (routeAdvisory) lines.push('', routeAdvisory);
112
114
  return lines.join('\n').trimEnd();
113
115
  }
114
116
 
@@ -220,6 +222,7 @@ function formatEndpointsJson(result, options = {}) {
220
222
  meta: {
221
223
  ok: true,
222
224
  ...meta,
225
+ ...(result.advisory && { advisory: result.advisory }),
223
226
  // HIGH-2: signal to consumers that bridges array was suppressed
224
227
  // because the user filtered to unmatched-only.
225
228
  ...(unmatchedOnly && { filterMode: 'unmatched' }),
@@ -17,11 +17,11 @@ function formatFn(match, fnCode) {
17
17
  lines.push(`${match.relativePath}:${match.startLine}`);
18
18
  // Class attribution: three same-name `clear` methods under --all were
19
19
  // indistinguishable without their owning class (fix #248).
20
- const sig = formatFunctionSignature(match);
21
- const attributed = match.className && !sig.includes(`${match.className}.`)
22
- ? `${match.className}.${sig}`
23
- : sig;
24
- lines.push(`${lineRange(match.startLine, match.endLine)} ${attributed}`);
20
+ const signatureTarget = match.className
21
+ ? { ...match, name: `${match.className}.${match.name}` }
22
+ : match;
23
+ const sig = formatFunctionSignature(signatureTarget);
24
+ lines.push(`${lineRange(match.startLine, match.endLine)} ${sig}`);
25
25
  lines.push('─'.repeat(60));
26
26
  lines.push(fnCode);
27
27
  return lines.join('\n');
@@ -69,10 +69,14 @@ function formatFunctionJson(fn, code) {
69
69
  * Notes are NOT included — surfaces render those separately (e.g. stderr for CLI).
70
70
  * @param {{ entries: Array<{match, code}>, notes: string[] }} result
71
71
  */
72
- function formatFnResult(result) {
72
+ function formatFnResult(result, options = {}) {
73
73
  const parts = [];
74
- for (const { match, code } of result.entries) {
75
- parts.push(formatFn(match, code));
74
+ for (const { match, code, truncated, shownLines, totalLines } of result.entries) {
75
+ let text = formatFn(match, code);
76
+ if (truncated) {
77
+ text += `\n... showing ${shownLines} of ${totalLines} lines (${options.maxLinesHint || 'use --max-lines=N or omit it for the full function'})`;
78
+ }
79
+ parts.push(text);
76
80
  }
77
81
  const separator = result.entries.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '';
78
82
  return parts.join(separator);
@@ -83,9 +87,16 @@ function formatFnResult(result) {
83
87
  */
84
88
  function formatFnResultJson(result) {
85
89
  if (result.entries.length === 1) {
86
- return formatFunctionJson(result.entries[0].match, result.entries[0].code);
90
+ const entry = result.entries[0];
91
+ const value = JSON.parse(formatFunctionJson(entry.match, entry.code));
92
+ if (entry.truncated) {
93
+ value.truncated = true;
94
+ value.shownLines = entry.shownLines;
95
+ value.totalLines = entry.totalLines;
96
+ }
97
+ return JSON.stringify(value, null, 2);
87
98
  }
88
- const arr = result.entries.map(({ match, code }) => ({
99
+ const arr = result.entries.map(({ match, code, truncated, shownLines, totalLines }) => ({
89
100
  name: match.name,
90
101
  params: match.params,
91
102
  paramsStructured: match.paramsStructured || [],
@@ -101,6 +112,7 @@ function formatFnResultJson(result) {
101
112
  ...(match.isGenerator && { isGenerator: true }),
102
113
  file: match.relativePath || match.file,
103
114
  code,
115
+ ...(truncated && { truncated: true, shownLines, totalLines }),
104
116
  }));
105
117
  return JSON.stringify(arr, null, 2);
106
118
  }
@@ -109,7 +121,7 @@ function formatFnResultJson(result) {
109
121
  * Format class handler result (from execute.js).
110
122
  * @param {{ entries: Array<{match, code, methods?, summaryMode, truncated, totalLines, maxLines?}>, notes: string[] }} result
111
123
  */
112
- function formatClassResult(result) {
124
+ function formatClassResult(result, options = {}) {
113
125
  const parts = [];
114
126
  for (const entry of result.entries) {
115
127
  if (entry.summaryMode) {
@@ -124,7 +136,7 @@ function formatClassResult(result) {
124
136
  lines.push(` ${formatFunctionSignature(m)} [line ${m.startLine}]`);
125
137
  }
126
138
  }
127
- lines.push(`\nClass is ${entry.totalLines} lines. Use --max-lines=N to see source, or "fn <method>" for individual methods.`);
139
+ lines.push(`\nClass is ${entry.totalLines} lines. ${options.classSourceHint || 'Use --max-lines=N to see source, or "source <method-handle>" for an individual method.'}`);
128
140
  parts.push(lines.join('\n'));
129
141
  } else if (entry.truncated) {
130
142
  parts.push(formatClass(entry.match, entry.code) + `\n\n... showing ${entry.maxLines} of ${entry.totalLines} lines`);