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
@@ -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,6 +238,7 @@ 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,
196
244
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -206,6 +254,7 @@ function formatContextJson(context) {
206
254
  unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
207
255
  file: c.relativePath || c.file,
208
256
  line: c.line,
257
+ ...(Number.isInteger(c.column) && { column: c.column }),
209
258
  expression: c.content,
210
259
  callerName: c.callerName ?? null,
211
260
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -219,6 +268,10 @@ function formatContextJson(context) {
219
268
  ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
220
269
  ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
221
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 }),
222
275
  })),
223
276
  ...(context.warnings && { warnings: context.warnings })
224
277
  }
@@ -242,6 +295,7 @@ function formatContextJson(context) {
242
295
  callers: callers.map(c => ({
243
296
  file: c.relativePath || c.file,
244
297
  line: c.line,
298
+ ...(Number.isInteger(c.column) && { column: c.column }),
245
299
  expression: c.content, // FULL expression
246
300
  callerName: c.callerName,
247
301
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -255,6 +309,7 @@ function formatContextJson(context) {
255
309
  unverifiedCallers: unverifiedCallers.map(c => ({
256
310
  file: c.relativePath || c.file,
257
311
  line: c.line,
312
+ ...(Number.isInteger(c.column) && { column: c.column }),
258
313
  expression: c.content, // FULL expression
259
314
  callerName: c.callerName ?? null,
260
315
  ...(c.calledAs && { calledAs: c.calledAs }),
@@ -267,6 +322,10 @@ function formatContextJson(context) {
267
322
  ...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
268
323
  ...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
269
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 }),
270
329
  })),
271
330
  callees: callees.map(c => ({
272
331
  name: c.name,
@@ -299,13 +358,14 @@ function formatContext(ctx, options = {}) {
299
358
  if (!ctx) return { text: 'Symbol not found.', expandable: [] };
300
359
 
301
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';
302
362
 
303
363
  const lines = [];
304
364
  const expandable = [];
305
365
  let itemNum = 1;
306
366
 
307
367
  // Handle struct/interface types
308
- 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)) {
309
369
  lines.push(`Context for ${ctx.type} ${ctx.name}:`);
310
370
  lines.push('═'.repeat(60));
311
371
 
@@ -334,8 +394,20 @@ function formatContext(ctx, options = {}) {
334
394
  });
335
395
  }
336
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
+
337
406
  const callers = ctx.callers || [];
338
- 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}):`);
339
411
  for (const c of callers) {
340
412
  const callerName = c.callerName ? ` [${c.callerName}]` : '';
341
413
  lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
@@ -376,7 +448,7 @@ function formatContext(ctx, options = {}) {
376
448
  shown++;
377
449
  }
378
450
  if (typeUnverified.length > shown) {
379
- lines.push(` (+${typeUnverified.length - shown} more unverified — use --all)`);
451
+ lines.push(` (+${typeUnverified.length - shown} more unverified — ${allHint})`);
380
452
  }
381
453
  }
382
454
 
@@ -424,9 +496,12 @@ function formatContext(ctx, options = {}) {
424
496
  const callers = ctx.callers || [];
425
497
  const prodCallers = callers.filter(c => !isTestEntry(c));
426
498
  const testCallers = callers.filter(c => isTestEntry(c));
427
- const tierHeader = testCallers.length > 0
428
- ? `CALLERS — CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
429
- : `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}):`;
430
505
  lines.push(`${compact ? '' : '\n'}${tierHeader}`);
431
506
  const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
432
507
  if (callerEvidence) lines.push(callerEvidence);
@@ -464,15 +539,123 @@ function formatContext(ctx, options = {}) {
464
539
  // candidates ARE analyzed — they render in the unverified band with
465
540
  // reasons, and the ACCOUNT line reconciles every text occurrence.
466
541
 
467
- // UNVERIFIED tier: call-syntax matches without binding/receiver evidence.
468
- // Always visible (the contract: never silently hide an occurrence), capped
469
- // 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.
470
548
  const unverified = ctx.unverifiedCallers || [];
471
- if (unverified.length > 0) {
472
- 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:`);
473
656
  const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
474
657
  let shown = 0;
475
- for (const u of unverified) {
658
+ for (const u of actionableUnverified) {
476
659
  if (shown >= cap) break;
477
660
  const callerName = u.callerName ? ` [${u.callerName}]` : '';
478
661
  const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
@@ -490,13 +673,16 @@ function formatContext(ctx, options = {}) {
490
673
  });
491
674
  shown++;
492
675
  }
493
- if (unverified.length > shown) {
494
- lines.push(` (+${unverified.length - shown} more unverified — use --all)`);
676
+ if (actionableUnverified.length > shown) {
677
+ lines.push(` (+${actionableUnverified.length - shown} more unverified — ${allHint})`);
495
678
  }
496
679
  }
497
680
 
498
681
  const callees = ctx.callees || [];
499
- 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}):`);
500
686
  const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
501
687
  if (calleeEvidence) lines.push(calleeEvidence);
502
688
  const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
@@ -535,7 +721,7 @@ function formatContext(ctx, options = {}) {
535
721
  // Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
536
722
  const account = ctx.meta && ctx.meta.account;
537
723
  if (account) {
538
- const nonCallLine = formatNonCallLine(account, ctx.function);
724
+ const nonCallLine = formatNonCallLine(account, ctx.function, options.usagesHint);
539
725
  if (nonCallLine) lines.push(`${compact ? '' : '\n'}${nonCallLine}`);
540
726
  const accountLines = formatAccountLines(account);
541
727
  if (accountLines.length > 0) {
@@ -567,6 +753,9 @@ function formatImpact(impact, options = {}) {
567
753
  if (!compact) lines.push('═'.repeat(60));
568
754
  lines.push(`${impact.file}:${impact.startLine}`);
569
755
  if (!compact) lines.push(impact.signature);
756
+ for (const warning of impact.warnings || []) {
757
+ lines.push(`Note: ${warning.message}`);
758
+ }
570
759
  if (!compact) lines.push('');
571
760
 
572
761
  // Summary (confirmed + unverified tiers reported separately)
@@ -693,6 +882,7 @@ function formatAbout(about, options = {}) {
693
882
  }
694
883
 
695
884
  const lines = [];
885
+ const allHint = options.allHint || 'use --all';
696
886
  const sym = about.symbol;
697
887
  const { expand, root, depth } = options;
698
888
 
@@ -809,7 +999,7 @@ function formatAbout(about, options = {}) {
809
999
  lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
810
1000
  }
811
1001
  if (aboutUnverified.total > aboutUnverified.top.length) {
812
- lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — use --all)`);
1002
+ lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — ${allHint})`);
813
1003
  }
814
1004
  }
815
1005
 
@@ -927,7 +1117,7 @@ function formatAbout(about, options = {}) {
927
1117
  lines.push(about.code);
928
1118
  } else if (about.code && compact) {
929
1119
  lines.push('');
930
- 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.`);
931
1121
  }
932
1122
 
933
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`);