ucn 3.8.23 → 3.8.25

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 (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +114 -11
  2. package/README.md +152 -156
  3. package/cli/index.js +363 -37
  4. package/core/analysis.js +936 -32
  5. package/core/bridge.js +1111 -0
  6. package/core/brief.js +408 -0
  7. package/core/cache.js +105 -5
  8. package/core/callers.js +72 -18
  9. package/core/check.js +200 -0
  10. package/core/discovery.js +57 -34
  11. package/core/entrypoints.js +638 -4
  12. package/core/execute.js +304 -5
  13. package/core/git-enrich.js +130 -0
  14. package/core/graph.js +24 -2
  15. package/core/output/analysis.js +157 -25
  16. package/core/output/brief.js +100 -0
  17. package/core/output/check.js +79 -0
  18. package/core/output/doctor.js +85 -0
  19. package/core/output/endpoints.js +239 -0
  20. package/core/output/extraction.js +2 -0
  21. package/core/output/find.js +126 -39
  22. package/core/output/graph.js +48 -15
  23. package/core/output/refactoring.js +103 -5
  24. package/core/output/reporting.js +63 -23
  25. package/core/output/search.js +110 -17
  26. package/core/output/shared.js +56 -2
  27. package/core/output.js +4 -0
  28. package/core/parser.js +8 -2
  29. package/core/project.js +39 -3
  30. package/core/registry.js +30 -14
  31. package/core/reporting.js +465 -2
  32. package/core/search.js +130 -10
  33. package/core/shared.js +101 -5
  34. package/core/tracing.js +16 -6
  35. package/core/verify.js +982 -95
  36. package/languages/go.js +91 -6
  37. package/languages/html.js +10 -0
  38. package/languages/java.js +151 -35
  39. package/languages/javascript.js +290 -33
  40. package/languages/python.js +78 -11
  41. package/languages/rust.js +267 -12
  42. package/languages/utils.js +315 -3
  43. package/mcp/server.js +91 -16
  44. package/package.json +9 -1
package/cli/index.js CHANGED
@@ -15,13 +15,132 @@ const { ProjectIndex } = require('../core/project');
15
15
  const { expandGlob, findProjectRoot } = require('../core/discovery');
16
16
  const output = require('../core/output');
17
17
  const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName, FILE_LOCAL_COMMANDS } = require('../core/registry');
18
+ const { looksLikeHandle, parseSymbolHandle } = require('../core/shared');
19
+
20
+ /**
21
+ * Convert a CLI argument that may be a stable handle into the symbol name
22
+ * that's appropriate for headers / "Usages of X" / "find Y" displays.
23
+ * Plain names pass through unchanged.
24
+ */
25
+ function nameForDisplay(arg) {
26
+ if (typeof arg !== 'string') return arg;
27
+ if (!looksLikeHandle(arg)) return arg;
28
+ const h = parseSymbolHandle(arg);
29
+ return h && h.name ? h.name : arg;
30
+ }
18
31
  const { execute } = require('../core/execute');
19
32
  const { ExpandCache } = require('../core/expand-cache');
20
33
 
21
34
  // Sentinel error for command failures that have already printed their message.
22
35
  // Thrown instead of process.exit(1) so finally blocks can run (cache save).
23
36
  class CommandError extends Error { constructor() { super(); } }
24
- function fail(msg) { console.error(msg); throw new CommandError(); }
37
+
38
+ // Thrown by validateNumericFlags when a numeric flag has a bad value.
39
+ // The CLI top-level catches this, prints the message, and exits 1. Interactive
40
+ // mode catches it inside its REPL try/catch and continues the session.
41
+ class FlagValidationError extends Error {
42
+ constructor(msg) { super(msg); this.name = 'FlagValidationError'; }
43
+ }
44
+
45
+ /**
46
+ * Validate that a raw flag value is a positive integer. Returns the parsed
47
+ * number when valid, or throws FlagValidationError. Callers pass `null`/`undefined`
48
+ * raw values through unchanged (no flag → no validation).
49
+ *
50
+ * @param {string|null|undefined} raw - The raw string captured from the CLI/interactive token.
51
+ * @param {string} flagName - The CLI flag name including dashes (e.g. "--top") for error messages.
52
+ * @param {object} [opts]
53
+ * @param {boolean} [opts.allowZero=false] - Whether 0 is a valid value (e.g. depth=0 may be meaningful).
54
+ * @param {number} [opts.cap=10000000] - Maximum accepted value (rejects 1e100 etc).
55
+ * @returns {number|undefined} The validated integer, or undefined when raw is null/undefined.
56
+ */
57
+ function validatePositiveInt(raw, flagName, { allowZero = false, cap = 10000000 } = {}) {
58
+ if (raw == null) return undefined;
59
+ const label = allowZero ? 'non-negative integer' : 'positive integer';
60
+ const trimmed = String(raw).trim();
61
+ if (trimmed === '') {
62
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
63
+ }
64
+ const n = Number(trimmed);
65
+ if (!isFinite(n) || isNaN(n)) {
66
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
67
+ }
68
+ if (!Number.isInteger(n)) {
69
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got ${n})`);
70
+ }
71
+ if (allowZero) {
72
+ if (n < 0) {
73
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got ${n})`);
74
+ }
75
+ } else if (n <= 0) {
76
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got ${n})`);
77
+ }
78
+ if (n > cap) {
79
+ throw new FlagValidationError(`Invalid ${flagName} value: ${n} exceeds maximum (${cap})`);
80
+ }
81
+ return n;
82
+ }
83
+
84
+ /**
85
+ * Validate all numeric flags on a parsed flags object. Looks at the *Raw
86
+ * companion strings preserved by parseFlags so we catch user-supplied bad
87
+ * values regardless of whether the parsed numeric form happened to be falsy.
88
+ * Mutates `flags` to hold the validated numeric values.
89
+ *
90
+ * Throws FlagValidationError on the first invalid flag.
91
+ */
92
+ function validateNumericFlags(flags) {
93
+ // --top: positive integer, no zero. Used by stats/find/context/etc.
94
+ if (flags.topRaw != null) {
95
+ flags.top = validatePositiveInt(flags.topRaw, '--top');
96
+ }
97
+ // --limit: positive integer, no zero. Reject "0 = no limit" silent coercion.
98
+ if (flags.limitRaw != null) {
99
+ flags.limit = validatePositiveInt(flags.limitRaw, '--limit');
100
+ }
101
+ // --max-files: positive integer, no zero.
102
+ if (flags.maxFilesRaw != null) {
103
+ flags.maxFiles = validatePositiveInt(flags.maxFilesRaw, '--max-files');
104
+ }
105
+ // --max-lines: positive integer, no zero. Used by class command.
106
+ if (flags.maxLinesRaw != null) {
107
+ flags.maxLines = validatePositiveInt(flags.maxLinesRaw, '--max-lines');
108
+ }
109
+ // --depth: non-negative integer (0 is meaningful: "this symbol only").
110
+ if (flags.depthRaw != null) {
111
+ flags.depth = validatePositiveInt(flags.depthRaw, '--depth', { allowZero: true });
112
+ }
113
+ // --context: non-negative integer (0 = no surrounding lines).
114
+ if (flags.contextRaw != null) {
115
+ flags.context = validatePositiveInt(flags.contextRaw, '--context', { allowZero: true });
116
+ }
117
+ // --workers: non-negative integer (0 disables parallel build).
118
+ if (flags.workersRaw != null) {
119
+ flags.workers = validatePositiveInt(flags.workersRaw, '--workers', { allowZero: true });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Print an error message and abort. When `--json` is in effect, write a JSON
125
+ * error envelope to stdout (so JSON-consuming pipelines see structured output)
126
+ * and write the same plain message to stderr (for humans piping to a TTY).
127
+ */
128
+ function fail(msg) {
129
+ // Honor --json by writing a structured envelope to stdout for pipelines.
130
+ // We use try/catch around symbol lookups because `flags` may not be initialized
131
+ // yet when fail() is called from the early arg-parsing path (TDZ).
132
+ let wantsJson = false;
133
+ try { if (typeof flags !== 'undefined' && flags && flags.json) wantsJson = true; } catch (_) {}
134
+ if (!wantsJson) {
135
+ try { if (Array.isArray(process.argv) && process.argv.includes('--json')) wantsJson = true; } catch (_) {}
136
+ }
137
+ if (wantsJson) {
138
+ const env = { meta: { ok: false }, error: typeof msg === 'string' ? msg : String(msg) };
139
+ try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) {}
140
+ }
141
+ console.error(msg);
142
+ throw new CommandError();
143
+ }
25
144
 
26
145
  // ============================================================================
27
146
  // ARGUMENT PARSING
@@ -74,10 +193,11 @@ function parseFlags(tokens) {
74
193
  exclude: parseExclude(),
75
194
  in: getValueFlag('--in'),
76
195
  includeTests: tokens.includes('--include-tests') ? true : undefined,
196
+ excludeTests: tokens.includes('--exclude-tests') ? true : undefined,
77
197
  includeExported: tokens.includes('--include-exported') || undefined,
78
198
  includeDecorated: tokens.includes('--include-decorated') || undefined,
79
199
  includeUncertain: tokens.includes('--include-uncertain') || undefined,
80
- includeMethods: tokens.some(a => a === '--include-methods=false') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
200
+ includeMethods: tokens.some(a => a === '--include-methods=false' || a === '--no-include-methods') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
81
201
  detailed: tokens.includes('--detailed') || undefined,
82
202
  topLevel: tokens.includes('--top-level') || undefined,
83
203
  all: tokens.includes('--all') || undefined,
@@ -88,8 +208,14 @@ function parseFlags(tokens) {
88
208
  withTypes: tokens.includes('--with-types') || undefined,
89
209
  expand: tokens.includes('--expand') || undefined,
90
210
  depth: getValueFlag('--depth'),
211
+ depthRaw: getValueFlag('--depth'),
212
+ // `top` is the parsed numeric value (NaN/0 default → falsy). `topRaw`
213
+ // preserves the original string so downstream validators can produce
214
+ // helpful errors for "abc"/"-1"/"0" instead of silently defaulting.
91
215
  top: parseInt(getValueFlag('--top') || '0'),
216
+ topRaw: getValueFlag('--top'),
92
217
  context: parseInt(getValueFlag('--context') || '0'),
218
+ contextRaw: getValueFlag('--context'),
93
219
  direction: getValueFlag('--direction'),
94
220
  addParam: getValueFlag('--add-param'),
95
221
  removeParam: getValueFlag('--remove-param'),
@@ -97,12 +223,20 @@ function parseFlags(tokens) {
97
223
  defaultValue: getValueFlag('--default'),
98
224
  base: getValueFlag('--base'),
99
225
  staged: tokens.includes('--staged') || undefined,
226
+ deep: tokens.includes('--deep') || undefined,
227
+ compact: tokens.includes('--compact') || undefined,
100
228
  maxLines: getValueFlag('--max-lines') || null,
229
+ maxLinesRaw: getValueFlag('--max-lines'),
101
230
  regex: tokens.includes('--no-regex') ? false : undefined,
102
231
  functions: tokens.includes('--functions') || undefined,
232
+ hot: tokens.includes('--hot') || undefined,
233
+ diverse: tokens.includes('--diverse') || undefined,
234
+ git: tokens.includes('--git') || undefined,
103
235
  className: getValueFlag('--class-name'),
104
236
  limit: parseInt(getValueFlag('--limit') || '0') || undefined,
237
+ limitRaw: getValueFlag('--limit'),
105
238
  maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
239
+ maxFilesRaw: getValueFlag('--max-files'),
106
240
  // Structural search flags
107
241
  type: getValueFlag('--type'),
108
242
  param: getValueFlag('--param'),
@@ -111,10 +245,20 @@ function parseFlags(tokens) {
111
245
  decorator: getValueFlag('--decorator'),
112
246
  exported: tokens.includes('--exported') || undefined,
113
247
  unused: tokens.includes('--unused') || undefined,
114
- showConfidence: tokens.includes('--no-confidence') ? false : undefined,
248
+ showConfidence: (tokens.includes('--hide-confidence') || tokens.includes('--no-confidence')) ? false : undefined,
115
249
  minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
250
+ unreachableOnly: tokens.includes('--unreachable-only') || undefined,
116
251
  framework: getValueFlag('--framework'),
252
+ // endpoints command flags
253
+ bridge: tokens.includes('--bridge') || undefined,
254
+ serverOnly: tokens.includes('--server-only') || undefined,
255
+ clientOnly: tokens.includes('--client-only') || undefined,
256
+ unmatched: tokens.includes('--unmatched') || undefined,
257
+ method: getValueFlag('--method'),
258
+ prefix: getValueFlag('--prefix'),
259
+ hideUncertain: tokens.includes('--hide-uncertain') || tokens.includes('--no-uncertain') || undefined,
117
260
  stack: getValueFlag('--stack'),
261
+ workersRaw: getValueFlag('--workers'),
118
262
  workers: (() => {
119
263
  const v = getValueFlag('--workers');
120
264
  if (v === null) return undefined;
@@ -138,17 +282,19 @@ const knownFlags = new Set([
138
282
  '--help', '-h', '--mcp',
139
283
  '--json', '--verbose', '--no-quiet', '--quiet',
140
284
  '--code-only', '--with-types', '--top-level', '--exact', '--case-sensitive',
141
- '--no-cache', '--clear-cache', '--include-tests',
142
- '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--include-uncertain', '--detailed', '--calls-only',
285
+ '--no-cache', '--clear-cache', '--include-tests', '--exclude-tests',
286
+ '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--detailed', '--calls-only',
143
287
  '--file', '--context', '--exclude', '--not', '--in',
144
288
  '--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
145
289
  '--default', '--top', '--no-follow-symlinks',
146
290
  '--base', '--staged', '--stack',
147
- '--regex', '--no-regex', '--functions',
291
+ '--regex', '--no-regex', '--functions', '--hot', '--diverse', '--git',
148
292
  '--max-lines', '--class-name', '--limit', '--max-files',
149
293
  '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
150
- '--show-confidence', '--no-confidence', '--min-confidence',
151
- '--framework', '--workers'
294
+ '--hide-confidence', '--no-confidence', '--min-confidence', '--unreachable-only',
295
+ '--framework', '--workers', '--deep', '--compact',
296
+ '--bridge', '--server-only', '--client-only', '--unmatched',
297
+ '--method', '--prefix', '--hide-uncertain', '--no-uncertain'
152
298
  ]);
153
299
 
154
300
  // Handle help flag
@@ -171,6 +317,23 @@ if (unknownFlags.length > 0) {
171
317
  process.exit(1);
172
318
  }
173
319
 
320
+ // Validate numeric flag values up front so bad input fails before we build
321
+ // any indexes. Applies to --top, --limit, --max-files, --max-lines, --depth,
322
+ // --context, --workers. Throws FlagValidationError with a helpful message.
323
+ try {
324
+ validateNumericFlags(flags);
325
+ } catch (e) {
326
+ if (e instanceof FlagValidationError) {
327
+ if (flags.json) {
328
+ const env = { meta: { ok: false }, error: e.message };
329
+ try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) {}
330
+ }
331
+ console.error(e.message);
332
+ process.exit(1);
333
+ }
334
+ throw e;
335
+ }
336
+
174
337
  // Value flags that consume the next token (space form: --flag value)
175
338
  const VALUE_FLAGS = new Set([
176
339
  '--file', '--depth', '--top', '--context', '--direction',
@@ -178,7 +341,7 @@ const VALUE_FLAGS = new Set([
178
341
  '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
179
342
  '--type', '--param', '--receiver', '--returns', '--decorator',
180
343
  '--limit', '--max-files', '--min-confidence', '--stack', '--framework',
181
- '--workers'
344
+ '--workers', '--method', '--prefix'
182
345
  ]);
183
346
 
184
347
  // Remove flags from args, then add args after -- (which are all positional)
@@ -479,7 +642,7 @@ function runProjectCommand(rootDir, command, arg) {
479
642
  // Map from camelCase flag name to CLI flag string
480
643
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
481
644
  // Flags that are global (not command-specific) — skip warning for these
482
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
645
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
483
646
  for (const [key, value] of Object.entries(flags)) {
484
647
  if (globalFlags.has(key)) continue;
485
648
  // Skip unset values (undefined, null, 0, empty array) — but NOT false (explicit negation)
@@ -512,7 +675,7 @@ function runProjectCommand(rootDir, command, arg) {
512
675
  if (note) console.error(note);
513
676
  printOutput(result,
514
677
  r => output.formatSymbolJson(r, arg),
515
- r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
678
+ r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all, compact: flags.compact })
516
679
  );
517
680
  break;
518
681
  }
@@ -521,19 +684,29 @@ function runProjectCommand(rootDir, command, arg) {
521
684
  const { ok, result, error, note } = execute(index, 'usages', { name: arg, ...flags });
522
685
  if (!ok) fail(error);
523
686
  if (note) console.error(note);
687
+ const displayName = nameForDisplay(arg);
524
688
  printOutput(result,
525
- r => output.formatUsagesJson(r, arg),
526
- r => output.formatUsages(r, arg)
689
+ r => output.formatUsagesJson(r, displayName),
690
+ r => output.formatUsages(r, displayName, { compact: flags.compact })
527
691
  );
528
692
  break;
529
693
  }
530
694
 
531
695
  case 'example': {
532
- const { ok, result, error } = execute(index, 'example', { name: arg, file: flags.file, className: flags.className });
696
+ const { ok, result, error, note } = execute(index, 'example', {
697
+ name: arg,
698
+ file: flags.file,
699
+ className: flags.className,
700
+ diverse: flags.diverse,
701
+ top: flags.top || undefined,
702
+ includeTests: flags.includeTests,
703
+ });
533
704
  if (!ok) fail(error);
705
+ if (note) console.error(note);
706
+ const displayName = nameForDisplay(arg);
534
707
  printOutput(result,
535
- r => output.formatExampleJson(r, arg),
536
- r => output.formatExample(r, arg)
708
+ r => output.formatExampleJson(r, displayName),
709
+ r => output.formatExample(r, displayName)
537
710
  );
538
711
  break;
539
712
  }
@@ -549,6 +722,7 @@ function runProjectCommand(rootDir, command, arg) {
549
722
  expandHint: 'Use "expand <N>" or --expand to see code for items',
550
723
  uncertainHint: 'use --include-uncertain to include all',
551
724
  showConfidence: flags.showConfidence !== false,
725
+ compact: !!flags.compact,
552
726
  });
553
727
  console.log(text);
554
728
 
@@ -577,7 +751,29 @@ function runProjectCommand(rootDir, command, arg) {
577
751
  match, itemNum: expandNum, itemCount: items.length, validateRoot: true
578
752
  });
579
753
  if (!ok) fail(error);
580
- console.log(result.text);
754
+ if (flags.json) {
755
+ // Honor --json: structured output with the expanded code + metadata.
756
+ const env = {
757
+ meta: { command: 'expand', item: expandNum },
758
+ data: {
759
+ item: expandNum,
760
+ ...(match && {
761
+ name: match.name,
762
+ type: match.type,
763
+ file: match.relativePath || match.file,
764
+ startLine: match.startLine,
765
+ endLine: match.endLine,
766
+ handle: match.relativePath && match.startLine && match.name
767
+ ? `${match.relativePath}:${match.startLine}:${match.name}`
768
+ : null,
769
+ }),
770
+ text: result.text,
771
+ },
772
+ };
773
+ console.log(JSON.stringify(env, null, 2));
774
+ } else {
775
+ console.log(result.text);
776
+ }
581
777
  break;
582
778
  }
583
779
 
@@ -596,7 +792,7 @@ function runProjectCommand(rootDir, command, arg) {
596
792
  if (!ok) fail(error);
597
793
  printOutput(result,
598
794
  output.formatAboutJson,
599
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false })
795
+ r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact, git: !!flags.git })
600
796
  );
601
797
  if (note) console.error(note);
602
798
  break;
@@ -605,7 +801,7 @@ function runProjectCommand(rootDir, command, arg) {
605
801
  case 'impact': {
606
802
  const { ok, result, error, note } = execute(index, 'impact', { name: arg, ...flags });
607
803
  if (!ok) fail(error);
608
- printOutput(result, output.formatImpactJson, output.formatImpact);
804
+ printOutput(result, output.formatImpactJson, r => output.formatImpact(r, { compact: flags.compact }));
609
805
  if (note) console.error(note);
610
806
  break;
611
807
  }
@@ -663,6 +859,34 @@ function runProjectCommand(rootDir, command, arg) {
663
859
  break;
664
860
  }
665
861
 
862
+ case 'brief': {
863
+ requireArg(arg, 'Usage: ucn . brief <name>');
864
+ const { ok, result, error } = execute(index, 'brief', { name: arg, file: flags.file, className: flags.className, git: flags.git });
865
+ if (!ok) fail(error);
866
+ printOutput(result, output.formatBriefJson, output.formatBrief);
867
+ break;
868
+ }
869
+
870
+ case 'doctor': {
871
+ const { ok, result, error } = execute(index, 'doctor', {
872
+ file: flags.file, in: flags.in,
873
+ limit: flags.limit, deep: flags.deep,
874
+ });
875
+ if (!ok) fail(error);
876
+ printOutput(result, output.formatDoctorJson, output.formatDoctor);
877
+ break;
878
+ }
879
+
880
+ case 'check': {
881
+ const { ok, result, error } = execute(index, 'check', {
882
+ base: flags.base, staged: flags.staged,
883
+ file: flags.file, limit: flags.limit,
884
+ });
885
+ if (!ok) fail(error);
886
+ printOutput(result, output.formatCheckJson, output.formatCheck);
887
+ break;
888
+ }
889
+
666
890
  // ── Extraction commands (via execute) ────────────────────────────
667
891
 
668
892
  case 'fn': {
@@ -760,9 +984,10 @@ function runProjectCommand(rootDir, command, arg) {
760
984
  case 'tests': {
761
985
  const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className, file: flags.file, exclude: flags.exclude });
762
986
  if (!ok) fail(error);
987
+ const displayName = nameForDisplay(arg);
763
988
  printOutput(result,
764
- r => output.formatTestsJson(r, arg),
765
- r => output.formatTests(r, arg)
989
+ r => output.formatTestsJson(r, displayName),
990
+ r => output.formatTests(r, displayName)
766
991
  );
767
992
  break;
768
993
  }
@@ -817,7 +1042,7 @@ function runProjectCommand(rootDir, command, arg) {
817
1042
  }
818
1043
 
819
1044
  case 'entrypoints': {
820
- const { ok, result, error, note } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file, exclude: flags.exclude, includeTests: flags.includeTests, limit: flags.limit });
1045
+ const { ok, result, error, note } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file, exclude: flags.exclude, includeTests: flags.includeTests, excludeTests: flags.excludeTests, limit: flags.limit });
821
1046
  if (!ok) fail(error);
822
1047
  if (note) console.error(note);
823
1048
  printOutput(result,
@@ -827,9 +1052,42 @@ function runProjectCommand(rootDir, command, arg) {
827
1052
  break;
828
1053
  }
829
1054
 
1055
+ case 'endpoints': {
1056
+ const { ok, result, error, note } = execute(index, 'endpoints', {
1057
+ file: flags.file,
1058
+ exclude: flags.exclude,
1059
+ limit: flags.limit,
1060
+ framework: flags.framework,
1061
+ bridge: flags.bridge,
1062
+ serverOnly: flags.serverOnly,
1063
+ clientOnly: flags.clientOnly,
1064
+ unmatched: flags.unmatched,
1065
+ method: flags.method,
1066
+ prefix: flags.prefix,
1067
+ hideUncertain: flags.hideUncertain,
1068
+ });
1069
+ if (!ok) fail(error);
1070
+ if (note) console.error(note);
1071
+ printOutput(result,
1072
+ output.formatEndpointsJson,
1073
+ r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched })
1074
+ );
1075
+ break;
1076
+ }
1077
+
830
1078
  case 'stats': {
831
- const { ok, result, error } = execute(index, 'stats', { functions: flags.functions });
1079
+ // MEDIUM-7: pass the raw --top value when present so the executor
1080
+ // can validate it and surface "Invalid --top" errors. Without
1081
+ // this, --top=abc is silently coerced to NaN → undefined and
1082
+ // the user gets the default (10) with no warning.
1083
+ const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
1084
+ const { ok, result, error, note } = execute(index, 'stats', {
1085
+ functions: flags.functions,
1086
+ hot: flags.hot,
1087
+ top: topVal,
1088
+ });
832
1089
  if (!ok) fail(error);
1090
+ if (note) console.error(note);
833
1091
  printOutput(result,
834
1092
  output.formatStatsJson,
835
1093
  r => output.formatStats(r, { top: flags.top })
@@ -845,6 +1103,18 @@ function runProjectCommand(rootDir, command, arg) {
845
1103
  break;
846
1104
  }
847
1105
 
1106
+ case 'auditAsync': {
1107
+ const { ok, result, error, note } = execute(index, 'auditAsync', {
1108
+ file: flags.file,
1109
+ exclude: flags.exclude,
1110
+ limit: flags.limit,
1111
+ });
1112
+ if (!ok) fail(error);
1113
+ if (note) console.error(note);
1114
+ printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
1115
+ break;
1116
+ }
1117
+
848
1118
  default:
849
1119
  console.error(`Unknown command: ${canonical}`);
850
1120
  printUsage();
@@ -858,8 +1128,10 @@ function runProjectCommand(rootDir, command, arg) {
858
1128
  } finally {
859
1129
  // Save cache after command execution so callsCache populated
860
1130
  // by findCallers/findCallees gets persisted to disk.
861
- // On cache-hit runs, only re-save if callsCache was mutated.
862
- if (flags.cache && (needsCacheSave || index.callsCacheDirty)) {
1131
+ // On cache-hit runs, only re-save if callsCache was mutated OR
1132
+ // reachability was computed (MED-1: persists the BFS result so
1133
+ // subsequent cold invocations don't repeat the 7-11s tax).
1134
+ if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty)) {
863
1135
  try { index.saveCache(); } catch (e) { /* best-effort */ }
864
1136
  }
865
1137
  }
@@ -966,7 +1238,7 @@ function runGlobCommand(pattern, command, arg) {
966
1238
  const applicableFlags = FLAG_APPLICABILITY[canonical];
967
1239
  if (applicableFlags) {
968
1240
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
969
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
1241
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
970
1242
  for (const [key, value] of Object.entries(flags)) {
971
1243
  if (globalFlags.has(key)) continue;
972
1244
  if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
@@ -1032,7 +1304,7 @@ function runGlobCommand(pattern, command, arg) {
1032
1304
  break;
1033
1305
  case 'about':
1034
1306
  printOutput(result, output.formatAboutJson,
1035
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false }));
1307
+ r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact }));
1036
1308
  break;
1037
1309
  case 'context':
1038
1310
  if (flags.json) {
@@ -1043,6 +1315,7 @@ function runGlobCommand(pattern, command, arg) {
1043
1315
  uncertainHint: 'use --include-uncertain to include all',
1044
1316
  expandHint: 'Use --expand to see inline callee previews',
1045
1317
  showConfidence: flags.showConfidence !== false,
1318
+ compact: !!flags.compact,
1046
1319
  });
1047
1320
  console.log(text);
1048
1321
  if (flags.expand) {
@@ -1060,6 +1333,15 @@ function runGlobCommand(pattern, command, arg) {
1060
1333
  printOutput(result, output.formatRelatedJson,
1061
1334
  r => output.formatRelated(r, { all: flags.all, top: flags.top }));
1062
1335
  break;
1336
+ case 'brief':
1337
+ printOutput(result, output.formatBriefJson, output.formatBrief);
1338
+ break;
1339
+ case 'doctor':
1340
+ printOutput(result, output.formatDoctorJson, output.formatDoctor);
1341
+ break;
1342
+ case 'check':
1343
+ printOutput(result, output.formatCheckJson, output.formatCheck);
1344
+ break;
1063
1345
  case 'trace':
1064
1346
  printOutput(result, output.formatTraceJson, output.formatTrace);
1065
1347
  break;
@@ -1115,9 +1397,15 @@ function runGlobCommand(pattern, command, arg) {
1115
1397
  case 'entrypoints':
1116
1398
  printOutput(result, output.formatEntrypointsJson, output.formatEntrypoints);
1117
1399
  break;
1400
+ case 'endpoints':
1401
+ printOutput(result, output.formatEndpointsJson, r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }));
1402
+ break;
1118
1403
  case 'diffImpact':
1119
1404
  printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
1120
1405
  break;
1406
+ case 'auditAsync':
1407
+ printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
1408
+ break;
1121
1409
  case 'stacktrace':
1122
1410
  printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
1123
1411
  break;
@@ -1141,6 +1429,8 @@ function runGlobCommand(pattern, command, arg) {
1141
1429
  // ============================================================================
1142
1430
 
1143
1431
 
1432
+ // Single source of truth for the public CLI help. README points here ("Run `ucn --help`")
1433
+ // rather than carrying a copy — keep it that way.
1144
1434
  function printUsage() {
1145
1435
  console.log(`UCN - Universal Code Navigator
1146
1436
 
@@ -1157,6 +1447,7 @@ Usage:
1157
1447
  UNDERSTAND CODE
1158
1448
  ═══════════════════════════════════════════════════════════════════════════════
1159
1449
  about <name> Full picture (definition, callers, callees, tests, code)
1450
+ brief <name> One-screen summary (signature, docstring, side effects, complexity)
1160
1451
  context <name> Who calls this + what it calls (numbered for expand)
1161
1452
  smart <name> Function + all dependencies inline
1162
1453
  impact <name> What breaks if changed (call sites grouped by file)
@@ -1200,16 +1491,22 @@ REFACTORING HELPERS
1200
1491
  plan <name> Preview refactoring (--add-param, --remove-param, --rename-to)
1201
1492
  verify <name> Check all call sites match signature
1202
1493
  diff-impact What changed in git diff and who calls it (--base, --staged)
1494
+ check Pre-commit summary: diff-impact + verify + affected-tests in one shot
1203
1495
  deadcode Find unused functions/classes
1204
1496
  entrypoints Detect framework entry points (routes, DI, tasks)
1497
+ endpoints HTTP API: list server routes + client requests; --bridge to match
1498
+ --bridge --server-only --client-only --unmatched
1499
+ --method=GET --prefix=/api --hide-uncertain
1205
1500
 
1206
1501
  ═══════════════════════════════════════════════════════════════════════════════
1207
1502
  OTHER
1208
1503
  ═══════════════════════════════════════════════════════════════════════════════
1209
1504
  api Show exported/public symbols
1210
1505
  typedef <name> Find type definitions
1211
- stats Project statistics (--functions for per-function line counts)
1506
+ stats Project statistics (--functions for per-function line counts, --hot for top callers)
1507
+ doctor Project trust report (counts, blind spots, parse failures, verdict; --deep for resolution coverage)
1212
1508
  stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
1509
+ audit-async Find calls in async functions that are likely missing await (JS/TS/Python)
1213
1510
 
1214
1511
  Common Flags:
1215
1512
  --file <pattern> Filter by file path (e.g., --file=routes)
@@ -1228,18 +1525,30 @@ Common Flags:
1228
1525
  --with-types Include type definitions (about, smart)
1229
1526
  --detailed Show all symbols in toc (not just counts)
1230
1527
  --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1528
+ --exclude-tests Exclude test files (entrypoints — tests are included by default)
1231
1529
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1232
1530
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1233
1531
  --include-uncertain Include ambiguous/uncertain matches
1234
- --no-confidence Hide confidence scores (shown by default in about, context)
1532
+ --hide-confidence Hide confidence scores (shown by default in about, context)
1235
1533
  --min-confidence=N Filter low-confidence edges (about, context, blast, trace,
1236
1534
  reverse-trace, smart, affected-tests)
1237
- --show-confidence Show confidence scores on caller/callee edges (about, context)
1535
+ --unreachable-only Show only callers/callees that are unreachable from entry points (about, context, impact)
1238
1536
  --include-exported Include exported symbols in deadcode
1239
1537
  --no-regex Force plain text search (regex is default)
1240
1538
  --functions Show per-function line counts (stats command)
1539
+ --hot Show top N most-called functions (stats command, pair with --top=N)
1540
+ --diverse Cluster call sites by argument shape (example command, pair with --top=N)
1541
+ --git Attach git enrichment (last modified, author, recent commits) to about/brief
1241
1542
  --include-decorated Include decorated/annotated symbols in deadcode
1242
1543
  --framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
1544
+ --bridge Match server routes to client requests (endpoints command).
1545
+ Confidence tiers: EXACT, PARTIAL, UNCERTAIN
1546
+ --server-only Only list server routes (endpoints command)
1547
+ --client-only Only list client requests (endpoints command)
1548
+ --unmatched Only show routes/requests with no match (endpoints, pair with --bridge)
1549
+ --method=X Filter by HTTP method (endpoints, e.g., --method=POST)
1550
+ --prefix=X Filter routes/requests by path prefix (endpoints, e.g., --prefix=/api)
1551
+ --hide-uncertain Hide UNCERTAIN-confidence bridges (endpoints command)
1243
1552
  --exact Exact name match only (find, typedef)
1244
1553
  --calls-only Only show call/test-case matches (tests)
1245
1554
  --case-sensitive Case-sensitive text search (search)
@@ -1357,7 +1666,7 @@ Flags can be added per-command: context myFunc --include-methods
1357
1666
  const tokens = input.split(/\s+/);
1358
1667
  const command = tokens[0];
1359
1668
  // Flags that take a space-separated value (--flag value)
1360
- const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--framework']);
1669
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--framework', '--method', '--prefix']);
1361
1670
  const flagTokens = [];
1362
1671
  const argTokens = [];
1363
1672
  const skipNext = new Set();
@@ -1378,10 +1687,18 @@ Flags can be added per-command: context myFunc --include-methods
1378
1687
  const iflags = parseFlags(flagTokens);
1379
1688
 
1380
1689
  try {
1690
+ // Validate numeric flags (--top, --limit, etc) — same rules as
1691
+ // global CLI mode. MED-2/MED-3/MED-5: bad values are rejected with
1692
+ // a helpful message instead of being silently coerced.
1693
+ validateNumericFlags(iflags);
1381
1694
  const iCanonical = resolveCommand(command, 'cli') || command;
1382
1695
  executeInteractiveCommand(index, iCanonical, arg, iflags, iExpandCache);
1383
1696
  } catch (e) {
1384
- console.error(`Error: ${e.message}`);
1697
+ if (e instanceof FlagValidationError) {
1698
+ console.log(e.message);
1699
+ } else {
1700
+ console.error(`Error: ${e.message}`);
1701
+ }
1385
1702
  }
1386
1703
 
1387
1704
  rl.prompt();
@@ -1406,14 +1723,15 @@ Flags can be added per-command: context myFunc --include-methods
1406
1723
 
1407
1724
  const INTERACTIVE_DISPATCH = {
1408
1725
  // ── Understanding Code ───────────────────────────────────────────
1409
- about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false }) },
1726
+ about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false, git: !!f.git }) },
1410
1727
  smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
1411
1728
  impact: { params: 'name', format: (r) => output.formatImpact(r) },
1412
1729
  blast: { params: 'name', format: (r) => output.formatBlast(r) },
1413
1730
  trace: { params: 'name', format: (r) => output.formatTrace(r) },
1414
1731
  reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
1415
1732
  related: { params: 'name', format: (r, _a, f) => output.formatRelated(r, { all: f.all, top: f.top }) },
1416
- example: { params: 'name', format: (r, a) => output.formatExample(r, a) },
1733
+ example: { params: (a, f) => ({ name: a, file: f.file, className: f.className, diverse: f.diverse, top: f.top || undefined, includeTests: f.includeTests }), format: (r, a) => output.formatExample(r, a) },
1734
+ brief: { params: 'name', format: (r) => output.formatBrief(r) },
1417
1735
 
1418
1736
  // ── Finding Code ─────────────────────────────────────────────────
1419
1737
  find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
@@ -1434,12 +1752,20 @@ const INTERACTIVE_DISPATCH = {
1434
1752
  plan: { params: 'name', format: (r) => output.formatPlan(r) },
1435
1753
  verify: { params: 'name', format: (r) => output.formatVerify(r) },
1436
1754
  diffImpact: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit, all: f.all }), format: (r, _a, f) => output.formatDiffImpact(r, { all: f.all }) },
1437
- entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude, includeTests: f.includeTests, limit: f.limit }), format: (r) => output.formatEntrypoints(r) },
1755
+ check: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit }), format: (r) => output.formatCheck(r) },
1756
+ entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude, includeTests: f.includeTests, excludeTests: f.excludeTests, limit: f.limit }), format: (r) => output.formatEntrypoints(r) },
1757
+ endpoints: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit, framework: f.framework, bridge: f.bridge, serverOnly: f.serverOnly, clientOnly: f.clientOnly, unmatched: f.unmatched, method: f.method, prefix: f.prefix, hideUncertain: f.hideUncertain }), format: (r) => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }) },
1438
1758
 
1439
1759
  // ── Other ────────────────────────────────────────────────────────
1440
1760
  api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file || '.') },
1441
1761
  stacktrace: { params: (a, f) => ({ stack: f.stack || a }), format: (r) => output.formatStackTrace(r) },
1442
- stats: { params: 'flags', format: (r, _a, f) => output.formatStats(r, { top: f.top }) },
1762
+ doctor: { params: (a, f) => ({ file: f.file, in: f.in, limit: f.limit, deep: f.deep }), format: (r) => output.formatDoctor(r) },
1763
+ // MED-2: stats handler in execute.js rejects top<=0; without explicit
1764
+ // coercion, parseFlags's `top: 0` default would surface as
1765
+ // "Invalid --top value" on bare `stats`. Mirror the project-mode top
1766
+ // coercion (topRaw when present, else undefined for default-10).
1767
+ stats: { params: (a, f) => ({ functions: f.functions, hot: f.hot, top: f.topRaw != null ? f.topRaw : (f.top || undefined) }), format: (r, _a, f) => output.formatStats(r, { top: f.top }) },
1768
+ auditAsync: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit }), format: (r) => output.formatAuditAsync(r) },
1443
1769
  };
1444
1770
 
1445
1771
  /**
@@ -1464,7 +1790,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1464
1790
  const applicableFlags = FLAG_APPLICABILITY[command];
1465
1791
  if (applicableFlags) {
1466
1792
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
1467
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
1793
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
1468
1794
  for (const [key, value] of Object.entries(iflags)) {
1469
1795
  if (globalFlags.has(key)) continue;
1470
1796
  if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;