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
package/cli/index.js CHANGED
@@ -14,22 +14,24 @@ const { detectLanguage } = require('../core/parser');
14
14
  const { ProjectIndex } = require('../core/project');
15
15
  const { expandGlob, findProjectRoot } = require('../core/discovery');
16
16
  const output = require('../core/output');
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
- }
17
+ const {
18
+ getCliCommandSet,
19
+ resolveCommand,
20
+ suggestCommand,
21
+ v4MigrationHint,
22
+ FLAG_APPLICABILITY,
23
+ toCliName,
24
+ FILE_LOCAL_COMMANDS,
25
+ formatSurfaceMessage,
26
+ getCliFlagsForCommand,
27
+ getCliAcceptedFlags,
28
+ } = require('../core/registry');
29
+ const { buildPublicParams, isPublicCommand } = require('../core/public-command');
31
30
  const { execute } = require('../core/execute');
32
- const { ExpandCache } = require('../core/expand-cache');
31
+ const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
32
+ const { clearAllCaches } = require('../core/cache');
33
+
34
+ let activeCanonicalCommand = null;
33
35
 
34
36
  // Sentinel error for command failures that have already printed their message.
35
37
  // Thrown instead of process.exit(1) so finally blocks can run (cache save).
@@ -42,6 +44,35 @@ class FlagValidationError extends Error {
42
44
  constructor(msg) { super(msg); this.name = 'FlagValidationError'; }
43
45
  }
44
46
 
47
+ function unknownCommandMessage(command, { interactive = false } = {}) {
48
+ const migration = v4MigrationHint(command, 'cli');
49
+ if (migration) return `Unknown command: ${command}. ${migration}`;
50
+ const suggestion = suggestCommand(command, 'cli');
51
+ const correction = suggestion ? ` Did you mean "${suggestion}"?` : '';
52
+ const help = !suggestion
53
+ ? (interactive
54
+ ? ' Type "help" for available commands.'
55
+ : ' Run "ucn --help" for available commands.')
56
+ : '';
57
+ return `Unknown command: ${command}.${correction}${help}`;
58
+ }
59
+
60
+ function resultExitCode(command, result) {
61
+ if (result && result.ok === false) return 2;
62
+ if (command !== 'check' || !result) return 0;
63
+ if (result.trust) {
64
+ // Target-less pre-commit check distinguishes advisory review from a
65
+ // blocking validation failure. Export/reference review remains
66
+ // machine-readable in trust.status but does not make every ordinary
67
+ // body edit fail CI; incomplete accounts and signature drift do.
68
+ return result.trust.status === 'BLOCKED' ? 1 : 0;
69
+ }
70
+ // Symbol-mode check (verify): a mismatch or unresolved call-site tier is
71
+ // review-required and must fail a CI gate.
72
+ return (Number(result.mismatches || 0) > 0 ||
73
+ Number(result.unverifiedCount || 0) > 0) ? 1 : 0;
74
+ }
75
+
45
76
  /**
46
77
  * Validate that a raw flag value is a positive integer. Returns the parsed
47
78
  * number when valid, or throws FlagValidationError. Callers pass `null`/`undefined`
@@ -61,6 +92,13 @@ function validatePositiveInt(raw, flagName, { allowZero = false, cap = 10000000
61
92
  if (trimmed === '') {
62
93
  throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
63
94
  }
95
+ // CLI integer flags use canonical base-10 spelling. Number()/parseInt()
96
+ // accept exponent, decimal and hexadecimal forms (`1e1`, `2.5`, `0x10`),
97
+ // which is especially dangerous for source-line identity because a
98
+ // malformed pin can accidentally select a real definition.
99
+ if (!/^\d+$/.test(trimmed)) {
100
+ throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
101
+ }
64
102
  const n = Number(trimmed);
65
103
  if (!isFinite(n) || isNaN(n)) {
66
104
  throw new FlagValidationError(`Invalid ${flagName} value: must be a ${label} (got "${raw}")`);
@@ -90,7 +128,7 @@ function validatePositiveInt(raw, flagName, { allowZero = false, cap = 10000000
90
128
  * Throws FlagValidationError on the first invalid flag.
91
129
  */
92
130
  function validateNumericFlags(flags) {
93
- // --top: positive integer, no zero. Used by stats/find/context/etc.
131
+ // --top: positive integer, no zero. Used by show/find/repo/etc.
94
132
  if (flags.topRaw != null) {
95
133
  flags.top = validatePositiveInt(flags.topRaw, '--top');
96
134
  }
@@ -102,7 +140,10 @@ function validateNumericFlags(flags) {
102
140
  if (flags.maxFilesRaw != null) {
103
141
  flags.maxFiles = validatePositiveInt(flags.maxFilesRaw, '--max-files');
104
142
  }
105
- // --max-lines: positive integer, no zero. Used by class command.
143
+ if (flags.lineRaw != null) {
144
+ flags.line = validatePositiveInt(flags.lineRaw, '--line');
145
+ }
146
+ // --max-lines: positive integer, no zero. Used by source.
106
147
  if (flags.maxLinesRaw != null) {
107
148
  flags.maxLines = validatePositiveInt(flags.maxLinesRaw, '--max-lines');
108
149
  }
@@ -118,22 +159,62 @@ function validateNumericFlags(flags) {
118
159
  if (flags.workersRaw != null) {
119
160
  flags.workers = validatePositiveInt(flags.workersRaw, '--workers', { allowZero: true });
120
161
  }
162
+ if (flags.maxCharsRaw != null) {
163
+ flags.maxChars = validatePositiveInt(flags.maxCharsRaw, '--max-chars', {
164
+ cap: MAX_OUTPUT_CHARS,
165
+ });
166
+ }
167
+ // --min-confidence: number in [0,1] (ordinal evidence weight). Anything
168
+ // else used to coerce to 0 silently — "abc" behaved like no filter.
169
+ if (flags.minConfidenceRaw != null) {
170
+ const value = Number(flags.minConfidenceRaw);
171
+ if (flags.minConfidenceRaw === '' || !Number.isFinite(value) ||
172
+ value < 0 || value > 1) {
173
+ throw new FlagValidationError(
174
+ `Invalid --min-confidence value: "${flags.minConfidenceRaw}" — must be a number between 0 and 1.`);
175
+ }
176
+ flags.minConfidence = value;
177
+ }
121
178
  }
122
179
 
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
- // This helper can run before parsed flags exist, so raw argv is the single
130
- // reliable source for the output mode.
180
+ /** Emit the one CLI error contract, including failures before main dispatch. */
181
+ function emitCliError(msg, command = activeCanonicalCommand) {
131
182
  const wantsJson = process.argv.includes('--json');
183
+ const error = typeof msg === 'string' ? msg : String(msg);
132
184
  if (wantsJson) {
133
- const env = { meta: { ok: false }, error: typeof msg === 'string' ? msg : String(msg) };
185
+ const canonical = command && isPublicCommand(command)
186
+ ? command
187
+ : resolveCommand(command, 'cli');
188
+ const surfaceCommand = canonical
189
+ ? toCliName(canonical)
190
+ : (command ? String(command) : null);
191
+ const env = {
192
+ meta: {
193
+ ok: false,
194
+ ...(surfaceCommand && {
195
+ command: surfaceCommand,
196
+ ...(canonical && surfaceCommand !== canonical && {
197
+ canonicalCommand: canonical,
198
+ }),
199
+ ...(canonical && {
200
+ contract: output.contractMeta(canonical),
201
+ }),
202
+ }),
203
+ },
204
+ data: null,
205
+ error,
206
+ };
134
207
  try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) { /* stdout may be closed */ }
135
208
  }
136
- console.error(msg);
209
+ console.error(error);
210
+ }
211
+
212
+ /**
213
+ * Print an error message and abort command execution. Throw instead of calling
214
+ * process.exit so index/cache finally blocks still run.
215
+ */
216
+ function fail(msg, command = activeCanonicalCommand) {
217
+ emitCliError(msg, command);
137
218
  throw new CommandError();
138
219
  }
139
220
 
@@ -191,7 +272,6 @@ function parseFlags(tokens) {
191
272
  excludeTests: tokens.includes('--exclude-tests') ? true : undefined,
192
273
  includeExported: tokens.includes('--include-exported') || undefined,
193
274
  includeDecorated: tokens.includes('--include-decorated') || undefined,
194
- includeUncertain: tokens.includes('--include-uncertain') || undefined,
195
275
  expandUnverified: tokens.includes('--expand-unverified') || undefined,
196
276
  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,
197
277
  detailed: tokens.includes('--detailed') || undefined,
@@ -202,7 +282,8 @@ function parseFlags(tokens) {
202
282
  codeOnly: tokens.includes('--code-only') || undefined,
203
283
  caseSensitive: tokens.includes('--case-sensitive') || undefined,
204
284
  withTypes: tokens.includes('--with-types') || undefined,
205
- expand: tokens.includes('--expand') || undefined,
285
+ withSource: tokens.includes('--with-source') || undefined,
286
+ cycles: tokens.includes('--cycles') || undefined,
206
287
  depth: getValueFlag('--depth'),
207
288
  depthRaw: getValueFlag('--depth'),
208
289
  // `top` is the parsed numeric value (NaN/0 default → falsy). `topRaw`
@@ -213,6 +294,9 @@ function parseFlags(tokens) {
213
294
  context: parseInt(getValueFlag('--context') || '0'),
214
295
  contextRaw: getValueFlag('--context'),
215
296
  direction: getValueFlag('--direction'),
297
+ to: getValueFlag('--to'),
298
+ sections: getValueFlag('--sections'),
299
+ range: getValueFlag('--range'),
216
300
  addParam: getValueFlag('--add-param'),
217
301
  removeParam: getValueFlag('--remove-param'),
218
302
  renameTo: getValueFlag('--rename-to'),
@@ -220,10 +304,14 @@ function parseFlags(tokens) {
220
304
  base: getValueFlag('--base'),
221
305
  staged: tokens.includes('--staged') || undefined,
222
306
  deep: tokens.includes('--deep') || undefined,
223
- compact: tokens.includes('--compact') || undefined,
307
+ compact: tokens.includes('--no-compact')
308
+ ? false
309
+ : (tokens.includes('--compact') ? true : undefined),
224
310
  maxLines: getValueFlag('--max-lines') || null,
225
311
  maxLinesRaw: getValueFlag('--max-lines'),
226
- regex: tokens.includes('--no-regex') ? false : undefined,
312
+ regex: tokens.includes('--regex')
313
+ ? true
314
+ : (tokens.includes('--no-regex') ? false : undefined),
227
315
  functions: tokens.includes('--functions') || undefined,
228
316
  hot: tokens.includes('--hot') || undefined,
229
317
  diverse: tokens.includes('--diverse') || undefined,
@@ -232,10 +320,13 @@ function parseFlags(tokens) {
232
320
  // Explicit line pin (fix #249: our own disambiguation notes advertise
233
321
  // line= but no surface accepted it).
234
322
  line: parseInt(getValueFlag('--line') || '0', 10) || undefined,
323
+ lineRaw: getValueFlag('--line'),
235
324
  limit: parseInt(getValueFlag('--limit') || '0') || undefined,
236
325
  limitRaw: getValueFlag('--limit'),
237
326
  maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
238
327
  maxFilesRaw: getValueFlag('--max-files'),
328
+ maxChars: parseInt(getValueFlag('--max-chars') || '0') || undefined,
329
+ maxCharsRaw: getValueFlag('--max-chars'),
239
330
  // Structural search flags
240
331
  type: getValueFlag('--type'),
241
332
  param: getValueFlag('--param'),
@@ -244,8 +335,10 @@ function parseFlags(tokens) {
244
335
  decorator: getValueFlag('--decorator'),
245
336
  exported: tokens.includes('--exported') || undefined,
246
337
  unused: tokens.includes('--unused') || undefined,
247
- showConfidence: (tokens.includes('--hide-confidence') || tokens.includes('--no-confidence')) ? false : undefined,
248
- minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
338
+ showConfidence: (tokens.includes('--hide-confidence') || tokens.includes('--no-confidence')) ? false
339
+ : tokens.includes('--show-confidence') ? true : undefined,
340
+ minConfidence: 0,
341
+ minConfidenceRaw: getValueFlag('--min-confidence'),
249
342
  unreachableOnly: tokens.includes('--unreachable-only') || undefined,
250
343
  framework: getValueFlag('--framework'),
251
344
  // endpoints command flags
@@ -277,24 +370,7 @@ flags.interactive = args.includes('--interactive') || args.includes('-i');
277
370
  flags.followSymlinks = !args.includes('--no-follow-symlinks');
278
371
 
279
372
  // Known flags for validation
280
- const knownFlags = new Set([
281
- '--help', '-h', '--version', '-v', '--mcp',
282
- '--json', '--verbose', '--no-quiet', '--quiet',
283
- '--code-only', '--with-types', '--top-level', '--exact', '--case-sensitive',
284
- '--no-cache', '--clear-cache', '--include-tests', '--exclude-tests',
285
- '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--expand-unverified', '--detailed', '--calls-only',
286
- '--file', '--context', '--exclude', '--not', '--in',
287
- '--depth', '--direction', '--add-param', '--remove-param', '--rename-to', '--default-value',
288
- '--default', '--top', '--no-follow-symlinks',
289
- '--base', '--staged', '--stack',
290
- '--regex', '--no-regex', '--functions', '--hot', '--diverse', '--git',
291
- '--max-lines', '--class-name', '--line', '--limit', '--max-files',
292
- '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
293
- '--hide-confidence', '--no-confidence', '--min-confidence', '--unreachable-only',
294
- '--framework', '--workers', '--deep', '--compact',
295
- '--bridge', '--server-only', '--client-only', '--unmatched',
296
- '--method', '--prefix', '--hide-uncertain', '--no-uncertain'
297
- ]);
373
+ const knownFlags = getCliAcceptedFlags();
298
374
 
299
375
  // Handle help flag
300
376
  if (args.includes('--help') || args.includes('-h')) {
@@ -317,8 +393,9 @@ const unknownFlags = args.filter(a => {
317
393
  });
318
394
 
319
395
  if (unknownFlags.length > 0) {
320
- console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
321
- console.error('Use --help to see available flags');
396
+ emitCliError(
397
+ `Unknown flag(s): ${unknownFlags.join(', ')}. Use --help to see available flags.`,
398
+ );
322
399
  process.exit(1);
323
400
  }
324
401
 
@@ -329,11 +406,7 @@ try {
329
406
  validateNumericFlags(flags);
330
407
  } catch (e) {
331
408
  if (e instanceof FlagValidationError) {
332
- if (flags.json) {
333
- const env = { meta: { ok: false }, error: e.message };
334
- try { process.stdout.write(JSON.stringify(env) + '\n'); } catch (_) { /* stdout may be closed */ }
335
- }
336
- console.error(e.message);
409
+ emitCliError(e.message);
337
410
  process.exit(1);
338
411
  }
339
412
  throw e;
@@ -341,11 +414,11 @@ try {
341
414
 
342
415
  // Value flags that consume the next token (space form: --flag value)
343
416
  const VALUE_FLAGS = new Set([
344
- '--file', '--depth', '--top', '--context', '--direction',
417
+ '--file', '--depth', '--top', '--context', '--direction', '--to', '--sections', '--range',
345
418
  '--add-param', '--remove-param', '--rename-to', '--default', '--default-value',
346
419
  '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name', '--line',
347
420
  '--type', '--param', '--receiver', '--returns', '--decorator',
348
- '--limit', '--max-files', '--min-confidence', '--stack', '--framework',
421
+ '--limit', '--max-files', '--max-chars', '--min-confidence', '--stack', '--framework',
349
422
  '--workers', '--method', '--prefix'
350
423
  ]);
351
424
 
@@ -378,58 +451,18 @@ function requireArg(arg, usage) {
378
451
  }
379
452
  }
380
453
 
381
- /**
382
- * Print result in JSON or text format based on --json flag
383
- * @param {*} result - The result data
384
- * @param {Function} jsonFn - Function to format as JSON (receives result)
385
- * @param {Function} textFn - Function to format as text (receives result)
386
- */
387
- function printOutput(result, jsonFn, textFn) {
388
- if (flags.json) {
389
- console.log(jsonFn(result));
390
- } else {
391
- const text = textFn(result);
392
- if (text !== undefined) {
393
- console.log(text);
394
- }
395
- }
396
- }
397
-
398
- /**
399
- * Print inline 3-line code previews for context items (--expand support).
400
- * Used by context in project, interactive, and glob modes. Previews both
401
- * directions with attribution headers (fix #252: callees only — the flag
402
- * was a silent no-op for caller-only contexts, and the unlabeled preview
403
- * rendered detached from the item it expanded).
404
- */
405
- function printInlineExpand(ctx, root) {
406
- if (!root || !ctx) return;
407
- const preview = (c, label) => {
408
- // Caller entries locate the enclosing FUNCTION via callerStartLine;
409
- // callee entries are definitions with startLine.
410
- const startLine = label === 'caller' ? c.callerStartLine : c.startLine;
411
- const endLine = label === 'caller'
412
- ? (c.callerEndLine || (startLine ? startLine + 5 : null))
413
- : (c.endLine || (startLine ? startLine + 5 : null));
414
- if (!c.relativePath || !startLine) return;
415
- try {
416
- const filePath = path.join(root, c.relativePath);
417
- const content = fs.readFileSync(filePath, 'utf-8');
418
- const codeLines = content.split('\n');
419
- console.log(` ┌ ${label}: ${c.name || c.callerName || '(anonymous)'} — ${c.relativePath}:${startLine}`);
420
- const previewLines = Math.min(3, endLine - startLine + 1);
421
- for (let i = 0; i < previewLines && startLine - 1 + i < codeLines.length; i++) {
422
- console.log(` │ ${codeLines[startLine - 1 + i]}`);
423
- }
424
- if (endLine - startLine + 1 > 3) {
425
- console.log(` │ ... (${endLine - startLine - 2} more lines)`);
426
- }
427
- } catch (e) {
428
- // Skip on error
429
- }
430
- };
431
- for (const c of ctx.callers || []) preview(c, 'caller');
432
- for (const c of ctx.callees || []) preview(c, 'callee');
454
+ function formatCliText(command, result, params, execution, displayFlags) {
455
+ const text = output.formatPublicText(command, result, params, {
456
+ ...execution,
457
+ surface: 'cli',
458
+ });
459
+ return applyOutputBudget(text, {
460
+ command,
461
+ maxChars: displayFlags?.maxChars,
462
+ all: !!displayFlags?.all,
463
+ surface: 'cli',
464
+ params,
465
+ }).text;
433
466
  }
434
467
 
435
468
  // ============================================================================
@@ -444,18 +477,36 @@ function main() {
444
477
  let target, command, arg;
445
478
 
446
479
  if (positionalArgs.length === 0) {
480
+ // Standalone `ucn --clear-cache` (the form SKILL.md and README
481
+ // document) clears the current project's cache — falling through to
482
+ // the help banner made it a silent no-op.
483
+ if (flags.clearCache) {
484
+ if (flags.all) {
485
+ const removed = clearAllCaches();
486
+ console.log(removed.length > 0
487
+ ? 'All UCN user caches cleared'
488
+ : 'No UCN user caches to clear');
489
+ process.exit(0);
490
+ }
491
+ const index = new ProjectIndex('.');
492
+ const removed = index.clearCache();
493
+ console.log(removed.length > 0
494
+ ? `Cache cleared (${index.root})`
495
+ : `No cache to clear (${index.root})`);
496
+ process.exit(0);
497
+ }
447
498
  // No args: show help
448
499
  printUsage();
449
500
  process.exit(0);
450
501
  } else if (positionalArgs.length === 1) {
451
- // One arg: could be a command (use . as target) or a target (use toc as command)
502
+ // One arg: could be a command (use . as target) or a target (use repo as command)
452
503
  if (COMMANDS.has(positionalArgs[0])) {
453
504
  target = '.';
454
505
  command = positionalArgs[0];
455
506
  arg = undefined;
456
507
  } else {
457
508
  target = positionalArgs[0];
458
- command = 'toc';
509
+ command = 'repo';
459
510
  arg = undefined;
460
511
  }
461
512
  } else if (COMMANDS.has(positionalArgs[0])) {
@@ -466,12 +517,10 @@ function main() {
466
517
  } else {
467
518
  // First arg is a target (path/glob)
468
519
  target = positionalArgs[0];
469
- command = positionalArgs[1] || 'toc';
520
+ command = positionalArgs[1] || 'repo';
470
521
  arg = positionalArgs[2];
471
- // lines takes `<file> <range>` as two positionals (fix #252 —
472
- // the extra token was silently dropped and the command then
473
- // demanded a --file the user had plainly given).
474
- if (command === 'lines' && positionalArgs.length > 3) {
522
+ // source accepts `<file> <range>` as two positionals.
523
+ if (command === 'source' && positionalArgs.length > 3) {
475
524
  arg = positionalArgs.slice(2).join(' ');
476
525
  }
477
526
  }
@@ -491,54 +540,62 @@ function main() {
491
540
  // Single file mode
492
541
  runFileCommand(target, command, arg);
493
542
  } else {
494
- console.error(`Error: "${target}" not found`);
495
- process.exit(1);
543
+ // `ucn missing-dir repo` is a target error. Any other two-token
544
+ // form whose first token is not a path is an unknown command.
545
+ const targetInvocation = positionalArgs.length > 1 &&
546
+ COMMANDS.has(positionalArgs[1]);
547
+ fail(
548
+ targetInvocation
549
+ ? `Error: "${target}" not found`
550
+ : unknownCommandMessage(target),
551
+ targetInvocation ? null : target,
552
+ );
496
553
  }
497
554
  } catch (e) {
498
555
  if (!(e instanceof CommandError)) {
499
- console.error(`Error: ${e.message}`);
556
+ emitCliError(`Error: ${e.message}`);
500
557
  }
501
558
  process.exitCode = 1;
502
559
  }
503
560
  }
504
561
 
505
562
  /**
506
- * Parse the lines command's target forms (fix #252: `lines main.ts 1-10`
507
- * silently dropped the second positional and demanded a --file that was
508
- * plainly given): `<range>` (+ --file), `<file> <range>`, `<file>:<range>`.
509
- */
510
- function parseLinesTarget(arg, fileFlag) {
511
- let file = fileFlag;
512
- let range = arg;
513
- const parts = String(arg || '').trim().split(/\s+/);
514
- if (parts.length === 2 && /^\d+(-\d+)?$/.test(parts[1])) {
515
- file = parts[0];
516
- range = parts[1];
517
- } else if (!/^\d+(-\d+)?$/.test(range)) {
518
- const m = String(range).match(/^(.+):(\d+(?:-\d+)?)$/);
519
- if (m) { file = m[1]; range = m[2]; }
520
- }
521
- return { file, range };
522
- }
523
-
524
- /**
525
- * Tiered-output contract notes: unverified callers are always shown for
526
- * these commands, so the legacy reveal flags are implied no-ops. Shared by
527
- * one-shot and interactive mode (fix #250 — interactive printed nothing).
563
+ * Tiered-output contract notes shared by one-shot and interactive mode.
528
564
  */
529
565
  function printTieredNoOpNotes(canonical, flags, print) {
530
- if (!['about', 'context', 'impact', 'trace', 'blast', 'reverseTrace', 'affectedTests', 'verify', 'smart'].includes(canonical)) return;
531
- if (flags.includeUncertain) {
532
- print(`Note: --include-uncertain has no effect on '${toCliName(canonical)}' — unverified candidates are always shown (tiered).`);
533
- }
534
- if (['impact', 'verify', 'blast', 'reverseTrace', 'affectedTests'].includes(canonical) && flags.includeMethods) {
566
+ if (!['show', 'impact', 'trace', 'tests', 'check'].includes(canonical)) return;
567
+ if (['impact', 'trace', 'tests', 'check'].includes(canonical) && flags.includeMethods) {
535
568
  print(`Note: --include-methods has no effect on '${toCliName(canonical)}' — method calls are always tiered by receiver evidence.`);
536
569
  }
537
- if (['about', 'context', 'smart'].includes(canonical) && flags.includeMethods) {
570
+ if (canonical === 'show' && flags.includeMethods) {
538
571
  print(`Note: --include-methods on '${toCliName(canonical)}' affects only method-callee display for standalone-function targets — caller tiers are always evidence-based, and method targets analyze method calls by default.`);
539
572
  }
540
573
  }
541
574
 
575
+ const GLOBAL_FLAG_KEYS = new Set([
576
+ 'json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles',
577
+ 'verbose', 'interactive', '_fileFromFileMode', 'topRaw',
578
+ 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw',
579
+ 'workers', 'workersRaw', 'lineRaw', 'maxChars', 'maxCharsRaw', 'minConfidenceRaw',
580
+ ]);
581
+
582
+ /** Apply one command/flag policy across project, file, glob, and REPL modes. */
583
+ function warnInapplicableFlags(canonical, parsedFlags, print) {
584
+ const applicableFlags = FLAG_APPLICABILITY[canonical];
585
+ if (!applicableFlags) return;
586
+ const flagToCli = (flag) => '--' + flag.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
587
+ for (const [key, value] of Object.entries(parsedFlags)) {
588
+ if (GLOBAL_FLAG_KEYS.has(key)) continue;
589
+ if (value === undefined || value === null || value === 0 ||
590
+ (Array.isArray(value) && value.length === 0)) continue;
591
+ if (key === 'file' && parsedFlags._fileFromFileMode) continue;
592
+ if (!applicableFlags.includes(key)) {
593
+ print(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
594
+ }
595
+ }
596
+ printTieredNoOpNotes(canonical, parsedFlags, print);
597
+ }
598
+
542
599
  // ============================================================================
543
600
  // FILE MODE
544
601
  // ============================================================================
@@ -546,11 +603,11 @@ function printTieredNoOpNotes(canonical, flags, print) {
546
603
  function runFileCommand(filePath, command, arg) {
547
604
  const language = detectLanguage(filePath);
548
605
  if (!language) {
549
- console.error(`Unsupported file type: ${filePath}`);
550
- process.exit(1);
606
+ fail(`Unsupported file type: ${filePath}`, command);
551
607
  }
552
608
 
553
609
  const canonical = resolveCommand(command, 'cli') || command;
610
+ activeCanonicalCommand = canonical;
554
611
 
555
612
  // Commands that need full project index — auto-route to project mode
556
613
  const fileLocalCommands = FILE_LOCAL_COMMANDS;
@@ -559,7 +616,7 @@ function runFileCommand(filePath, command, arg) {
559
616
  // Auto-detect project root and route to project mode
560
617
  const projectRoot = findProjectRoot(path.dirname(filePath));
561
618
  let effectiveArg = arg;
562
- if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
619
+ if (canonical === 'deps' && !arg) {
563
620
  effectiveArg = filePath;
564
621
  }
565
622
  // Scope to the target file unless an explicit --file was provided
@@ -572,78 +629,47 @@ function runFileCommand(filePath, command, arg) {
572
629
  return;
573
630
  }
574
631
 
575
- // Require arg for commands that need it
576
- const needsArg = { fn: 'fn <name>', class: 'class <name>', find: 'find <name>', usages: 'usages <name>', search: 'search <term>', lines: 'lines <start-end>', typedef: 'typedef <name>' };
632
+ if (!isPublicCommand(canonical)) {
633
+ fail(unknownCommandMessage(command));
634
+ }
635
+
636
+ // Require arg for commands that need it.
637
+ const needsArg = {
638
+ show: 'show <name>', find: 'find <name>', usages: 'usages <name>',
639
+ search: 'search <term>', source: 'source <name|range>', trace: 'trace <name>',
640
+ tests: 'tests <name>', plan: 'plan <name>',
641
+ };
577
642
  // Structural search doesn't require term
578
643
  const isStructural = flags.type || flags.param || flags.receiver || flags.returns || flags.decorator || flags.exported || flags.unused;
579
- if (needsArg[canonical] && !(canonical === 'search' && isStructural)) {
644
+ const hasFlagTarget = canonical === 'source' && flags.range;
645
+ if (needsArg[canonical] && !(canonical === 'search' && isStructural) && !hasFlagTarget) {
580
646
  requireArg(arg, `Usage: ucn <file> ${needsArg[canonical]}`);
581
647
  }
582
648
 
583
- // Build single-file index and route through execute()
649
+ // Build single-file index and route through the same public executor and
650
+ // formatter used by project/glob/MCP modes.
584
651
  const index = new ProjectIndex(path.dirname(filePath));
585
652
  index.buildSingleFile(filePath);
586
653
  const relativePath = path.relative(index.root, path.resolve(filePath));
587
654
 
588
- // Map command args to execute() params
589
- const paramsByCommand = {
590
- toc: { ...flags },
591
- fn: { name: arg, file: relativePath, ...flags },
592
- class: { name: arg, file: relativePath, ...flags },
593
- find: { name: arg, file: relativePath, ...flags },
594
- usages: { name: arg, file: relativePath, ...flags },
595
- search: { term: arg, ...flags },
596
- lines: { file: relativePath, range: arg },
597
- typedef: { name: arg, file: relativePath, ...flags },
598
- api: { file: relativePath, limit: flags.limit },
599
- };
600
-
601
- const { ok, result, error, note } = execute(index, canonical, paramsByCommand[canonical]);
602
- if (!ok) fail(error);
603
- if (note) console.error(note);
604
-
605
- // Format output using same formatters as project mode
606
- switch (canonical) {
607
- case 'toc':
608
- printOutput(result, output.formatTocJson, r => output.formatToc(r, {
609
- detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
610
- uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
611
- }));
612
- break;
613
- case 'find':
614
- printOutput(result,
615
- r => output.formatSymbolJson(r, arg),
616
- r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
617
- );
618
- break;
619
- case 'fn':
620
- printOutput(result, output.formatFnResultJson, output.formatFnResult);
621
- break;
622
- case 'class':
623
- printOutput(result, output.formatClassResultJson, output.formatClassResult);
624
- break;
625
- case 'lines':
626
- printOutput(result, output.formatLinesJson, r => output.formatLines(r));
627
- break;
628
- case 'usages':
629
- printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
630
- break;
631
- case 'search':
632
- if (result && result.meta && result.meta.mode === 'structural') {
633
- printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
634
- } else {
635
- printOutput(result, r => output.formatSearchJson(r, arg), r => output.formatSearch(r, arg));
636
- }
637
- break;
638
- case 'typedef':
639
- printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
640
- break;
641
- case 'api': {
642
- const apiFile = relativePath;
643
- printOutput(result, r => output.formatApiJson(r, apiFile), r => output.formatApi(r, apiFile));
644
- break;
645
- }
655
+ const scopedFlags = { ...flags };
656
+ if (['show', 'find', 'usages', 'source', 'api'].includes(canonical) && !scopedFlags.file) {
657
+ scopedFlags.file = relativePath;
646
658
  }
659
+ if (canonical === 'source' && /^\d+(?:-\d+)?$/.test(String(arg || ''))) {
660
+ scopedFlags.range = arg;
661
+ arg = undefined;
662
+ }
663
+ warnInapplicableFlags(canonical, scopedFlags, (message) => console.error(message));
664
+ const params = buildPublicParams(canonical, arg, scopedFlags);
665
+ const execution = execute(index, canonical, params);
666
+ const { ok, result, error } = execution;
667
+ if (!ok) fail(formatSurfaceMessage(error, 'cli'));
668
+ console.log(flags.json
669
+ ? output.formatPublicJson(canonical, result, params, {
670
+ ...execution, surface: 'cli',
671
+ })
672
+ : formatCliText(canonical, result, params, execution, scopedFlags));
647
673
  }
648
674
 
649
675
  // ============================================================================
@@ -662,12 +688,9 @@ function runProjectCommand(rootDir, command, arg) {
662
688
 
663
689
  // Clear cache if requested
664
690
  if (flags.clearCache) {
665
- const cacheDir = path.join(index.root, '.ucn-cache');
666
- if (fs.existsSync(cacheDir)) {
667
- fs.rmSync(cacheDir, { recursive: true, force: true });
668
- if (!flags.quiet) {
669
- console.error('Cache cleared');
670
- }
691
+ const removed = index.clearCache();
692
+ if (removed.length > 0 && !flags.quiet) {
693
+ console.error('Cache cleared');
671
694
  }
672
695
  }
673
696
 
@@ -676,8 +699,8 @@ function runProjectCommand(rootDir, command, arg) {
676
699
  let cacheWasLoaded = false;
677
700
  if (flags.cache && !flags.clearCache) {
678
701
  const loaded = index.loadCache();
679
- if (loaded) {
680
- cacheWasLoaded = true;
702
+ cacheWasLoaded = !!loaded;
703
+ if (loaded && !flags.maxFiles) {
681
704
  if (!index.isCacheStale()) {
682
705
  usedCache = true;
683
706
  if (!flags.quiet) {
@@ -693,516 +716,39 @@ function runProjectCommand(rootDir, command, arg) {
693
716
  if (!usedCache) {
694
717
  index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers });
695
718
  needsCacheSave = flags.cache;
696
- // Clear stale expand cache — line ranges may have shifted after rebuild
697
- try {
698
- const expandPath = path.join(index.root, '.ucn-cache', 'expandable.json');
699
- if (fs.existsSync(expandPath)) fs.unlinkSync(expandPath);
700
- } catch (_) { /* best-effort */ }
701
719
  }
702
720
 
703
721
  try {
704
- // Resolve CLI aliases to canonical command names — dispatch on canonical
722
+ // Resolve CLI spelling to the canonical command ID.
705
723
  const canonical = resolveCommand(command, 'cli') || command;
724
+ activeCanonicalCommand = canonical;
706
725
 
707
- // Warn about flags that don't apply to this command
708
- const applicableFlags = FLAG_APPLICABILITY[canonical];
709
- if (applicableFlags) {
710
- // Map from camelCase flag name to CLI flag string
711
- const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
712
- // Flags that are global (not command-specific) — skip warning for these
713
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
714
- for (const [key, value] of Object.entries(flags)) {
715
- if (globalFlags.has(key)) continue;
716
- // Skip unset values (undefined, null, 0, empty array) — but NOT false (explicit negation)
717
- if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
718
- // Skip --file when it was injected by file-mode routing, not user input
719
- if (key === 'file' && flags._fileFromFileMode) continue;
720
- if (!applicableFlags.includes(key)) {
721
- console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
722
- }
723
- }
724
- // Tiered-output contract notes (shared with interactive mode).
725
- printTieredNoOpNotes(canonical, flags, (m) => console.error(m));
726
+ if (!isPublicCommand(canonical)) {
727
+ fail(unknownCommandMessage(command));
726
728
  }
729
+ warnInapplicableFlags(canonical, flags, (message) => console.error(message));
727
730
 
728
- switch (canonical) {
729
- // ── Commands using shared executor ───────────────────────────────
730
-
731
- case 'toc': {
732
- const { ok, result, error, note } = execute(index, 'toc', flags);
733
- if (!ok) fail(error);
734
- if (note) console.error(note);
735
- printOutput(result, output.formatTocJson, r => output.formatToc(r, {
736
- detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
737
- uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
738
- }));
739
- break;
740
- }
741
-
742
- case 'find': {
743
- const { ok, result, error, note } = execute(index, 'find', { name: arg, ...flags });
744
- if (!ok) fail(error);
745
- if (note) console.error(note);
746
- printOutput(result,
747
- r => output.formatSymbolJson(r, arg),
748
- r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all, compact: flags.compact })
749
- );
750
- break;
751
- }
752
-
753
- case 'usages': {
754
- const { ok, result, error, note } = execute(index, 'usages', { name: arg, ...flags });
755
- if (!ok) fail(error);
756
- if (note) console.error(note);
757
- const displayName = nameForDisplay(arg);
758
- printOutput(result,
759
- r => output.formatUsagesJson(r, displayName),
760
- r => output.formatUsages(r, displayName, { compact: flags.compact })
761
- );
762
- break;
763
- }
764
-
765
- case 'example': {
766
- const { ok, result, error, note } = execute(index, 'example', {
767
- name: arg,
768
- file: flags.file,
769
- className: flags.className,
770
- diverse: flags.diverse,
771
- top: flags.top || undefined,
772
- includeTests: flags.includeTests,
773
- });
774
- if (!ok) fail(error);
775
- if (note) console.error(note);
776
- const displayName = nameForDisplay(arg);
777
- printOutput(result,
778
- r => output.formatExampleJson(r, displayName),
779
- r => output.formatExample(r, displayName)
780
- );
781
- break;
782
- }
783
-
784
- case 'context': {
785
- const { ok, result: ctx, error, note } = execute(index, 'context', { name: arg, ...flags });
786
- if (!ok) fail(error);
787
- if (flags.json) {
788
- console.log(output.formatContextJson(ctx));
789
- } else {
790
- const { text, expandable } = output.formatContext(ctx, {
791
- expandHint: 'Use "expand <N>" or --expand to see code for items',
792
- showConfidence: flags.showConfidence !== false,
793
- compact: !!flags.compact,
794
- });
795
- console.log(text);
796
-
797
- // Inline expansion of callees when --expand flag is set
798
- if (flags.expand) {
799
- printInlineExpand(ctx, index.root);
800
- }
801
-
802
- // Save expandable items to cache for 'expand' command
803
- saveExpandableItems(expandable, index.root);
804
- if (note) console.error(note);
805
- }
806
- break;
807
- }
808
-
809
- case 'expand': {
810
- requireArg(arg, 'Usage: ucn . expand <N>\nFirst run "ucn . context <name>" to get numbered items');
811
- // Whole-token integers only (fix #248: parseInt truncation made
812
- // `expand 1abc` and `expand 2.5` silently expand items 1 and 2).
813
- if (!/^\d+$/.test(String(arg).trim())) {
814
- fail(`Invalid item number: "${arg}"`);
815
- }
816
- const expandNum = parseInt(arg, 10);
817
- const cached = loadExpandableItems(index.root);
818
- const items = cached?.items || [];
819
- const match = items.find(i => i.num === expandNum);
820
- const { ok, result, error } = execute(index, 'expand', {
821
- match, itemNum: expandNum, itemCount: items.length, validateRoot: true
822
- });
823
- if (!ok) fail(error);
824
- if (flags.json) {
825
- // Honor --json: structured output with the expanded code + metadata.
826
- const env = {
827
- meta: { command: 'expand', item: expandNum },
828
- data: {
829
- item: expandNum,
830
- ...(match && {
831
- name: match.name,
832
- type: match.type,
833
- file: match.relativePath || match.file,
834
- startLine: match.startLine,
835
- endLine: match.endLine,
836
- handle: match.relativePath && match.startLine && match.name
837
- ? `${match.relativePath}:${match.startLine}:${match.name}`
838
- : null,
839
- }),
840
- text: result.text,
841
- },
842
- };
843
- console.log(JSON.stringify(env, null, 2));
844
- } else {
845
- console.log(result.text);
846
- }
847
- break;
848
- }
849
-
850
- case 'smart': {
851
- const { ok, result, error, note } = execute(index, 'smart', { name: arg, ...flags });
852
- if (!ok) fail(error);
853
- printOutput(result, output.formatSmartJson, r => output.formatSmart(r, {
854
- uncertainHint: 'unverified callees are listed below with reasons'
855
- }));
856
- if (note) console.error(note);
857
- break;
858
- }
859
-
860
- case 'about': {
861
- const { ok, result, error, note } = execute(index, 'about', { name: arg, ...flags });
862
- if (!ok) fail(error);
863
- printOutput(result,
864
- output.formatAboutJson,
865
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact, git: !!flags.git })
866
- );
867
- if (note) console.error(note);
868
- break;
869
- }
870
-
871
- case 'impact': {
872
- const { ok, result, error, note } = execute(index, 'impact', { name: arg, ...flags });
873
- if (!ok) fail(error);
874
- printOutput(result, output.formatImpactJson, r => output.formatImpact(r, { compact: flags.compact }));
875
- if (note) console.error(note);
876
- break;
877
- }
878
-
879
- case 'blast': {
880
- const { ok, result, error, note } = execute(index, 'blast', { name: arg, ...flags });
881
- if (!ok) fail(error);
882
- printOutput(result, output.formatBlastJson, output.formatBlast);
883
- if (note) console.error(note);
884
- break;
885
- }
886
-
887
- case 'plan': {
888
- const { ok, result, error } = execute(index, 'plan', { name: arg, ...flags });
889
- if (!ok) fail(error);
890
- printOutput(result, output.formatPlanJson, output.formatPlan);
891
- break;
892
- }
893
-
894
- case 'trace': {
895
- const { ok, result, error, note } = execute(index, 'trace', { name: arg, ...flags });
896
- if (!ok) fail(error);
897
- printOutput(result, output.formatTraceJson, output.formatTrace);
898
- if (note) console.error(note);
899
- break;
900
- }
901
-
902
- case 'reverseTrace': {
903
- const { ok, result, error, note } = execute(index, 'reverseTrace', { name: arg, ...flags });
904
- if (!ok) fail(error);
905
- printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
906
- if (note) console.error(note);
907
- break;
908
- }
909
-
910
- case 'stacktrace': {
911
- const { ok, result, error } = execute(index, 'stacktrace', { stack: flags.stack || arg });
912
- if (!ok) fail(error);
913
- printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
914
- break;
915
- }
916
-
917
- case 'verify': {
918
- const { ok, result, error } = execute(index, 'verify', { name: arg, ...flags });
919
- if (!ok) fail(error);
920
- printOutput(result, output.formatVerifyJson, output.formatVerify);
921
- break;
922
- }
923
-
924
- case 'related': {
925
- const { ok, result, error, note } = execute(index, 'related', { name: arg, ...flags });
926
- if (!ok) fail(error);
927
- printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { all: flags.all, top: flags.top }));
928
- if (note) console.error(note);
929
- break;
930
- }
931
-
932
- case 'brief': {
933
- requireArg(arg, 'Usage: ucn . brief <name>');
934
- const { ok, result, error } = execute(index, 'brief', { name: arg, file: flags.file, className: flags.className, line: flags.line, git: flags.git });
935
- if (!ok) fail(error);
936
- printOutput(result, output.formatBriefJson, output.formatBrief);
937
- break;
938
- }
939
-
940
- case 'doctor': {
941
- const { ok, result, error } = execute(index, 'doctor', {
942
- file: flags.file, in: flags.in,
943
- limit: flags.limit, deep: flags.deep,
944
- });
945
- if (!ok) fail(error);
946
- printOutput(result, output.formatDoctorJson, output.formatDoctor);
947
- break;
948
- }
949
-
950
- case 'orient': {
951
- const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
952
- const { ok, result, error } = execute(index, 'orient', { top: topVal });
953
- if (!ok) fail(error);
954
- printOutput(result, output.formatOrientJson, output.formatOrient);
955
- break;
956
- }
957
-
958
- case 'check': {
959
- const { ok, result, error } = execute(index, 'check', {
960
- base: flags.base, staged: flags.staged,
961
- file: flags.file, limit: flags.limit,
962
- });
963
- if (!ok) fail(error);
964
- printOutput(result, output.formatCheckJson, output.formatCheck);
965
- break;
966
- }
967
-
968
- // ── Extraction commands (via execute) ────────────────────────────
969
-
970
- case 'fn': {
971
- requireArg(arg, 'Usage: ucn . fn <name>');
972
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all, className: flags.className, line: flags.line });
973
- if (!ok) fail(error);
974
- if (note) console.error(note);
975
- printOutput(result, output.formatFnResultJson, output.formatFnResult);
976
- break;
977
- }
978
-
979
- case 'class': {
980
- requireArg(arg, 'Usage: ucn . class <name>');
981
- const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines, line: flags.line });
982
- if (!ok) fail(error);
983
- if (note) console.error(note);
984
- printOutput(result, output.formatClassResultJson, output.formatClassResult);
985
- break;
986
- }
987
-
988
- case 'lines': {
989
- requireArg(arg, 'Usage: ucn . lines <range> --file <path> (or: lines <file> <range>)');
990
- const linesTarget = parseLinesTarget(arg, flags.file);
991
- const { ok, result, error, note } = execute(index, 'lines', { file: linesTarget.file, range: linesTarget.range });
992
- if (!ok) fail(error);
993
- if (note) console.error(note);
994
- printOutput(result, output.formatLinesJson, r => output.formatLines(r));
995
- break;
996
- }
997
-
998
- // ── File dependency commands ────────────────────────────────────
999
-
1000
- case 'imports': {
1001
- const filePath = arg || flags.file;
1002
- const { ok, result, error } = execute(index, 'imports', { file: filePath });
1003
- if (!ok) fail(error);
1004
- printOutput(result,
1005
- r => output.formatImportsJson(r, filePath),
1006
- r => output.formatImports(r, filePath)
1007
- );
1008
- break;
1009
- }
1010
-
1011
- case 'exporters': {
1012
- const filePath = arg || flags.file;
1013
- const { ok, result, error } = execute(index, 'exporters', { file: filePath });
1014
- if (!ok) fail(error);
1015
- printOutput(result,
1016
- r => output.formatExportersJson(r, filePath),
1017
- r => output.formatExporters(r, filePath)
1018
- );
1019
- break;
1020
- }
1021
-
1022
- case 'fileExports': {
1023
- const filePath = arg || flags.file;
1024
- const { ok, result, error } = execute(index, 'fileExports', { file: filePath });
1025
- if (!ok) fail(error);
1026
- printOutput(result,
1027
- r => output.formatFileExportsJson(r, filePath),
1028
- r => output.formatFileExports(r, filePath)
1029
- );
1030
- break;
1031
- }
1032
-
1033
- case 'graph': {
1034
- const filePath = arg || flags.file;
1035
- const { ok, result, error } = execute(index, 'graph', { file: filePath, direction: flags.direction, depth: flags.depth, all: flags.all });
1036
- if (!ok) fail(error);
1037
- printOutput(result,
1038
- output.formatGraphJson,
1039
- r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: filePath })
1040
- );
1041
- break;
1042
- }
1043
-
1044
- case 'circularDeps': {
1045
- const { ok, result, error } = execute(index, 'circularDeps', { file: flags.file, exclude: flags.exclude });
1046
- if (!ok) fail(error);
1047
- printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
1048
- break;
1049
- }
1050
-
1051
- // ── Remaining commands ──────────────────────────────────────────
1052
-
1053
- case 'typedef': {
1054
- const { ok, result, error } = execute(index, 'typedef', { name: arg, exact: flags.exact, file: flags.file, className: flags.className });
1055
- if (!ok) fail(error);
1056
- printOutput(result,
1057
- r => output.formatTypedefJson(r, arg),
1058
- r => output.formatTypedef(r, arg)
1059
- );
1060
- break;
1061
- }
1062
-
1063
- case 'tests': {
1064
- const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className, file: flags.file, exclude: flags.exclude });
1065
- if (!ok) fail(error);
1066
- const displayName = nameForDisplay(arg);
1067
- printOutput(result,
1068
- r => output.formatTestsJson(r, displayName),
1069
- r => output.formatTests(r, displayName)
1070
- );
1071
- break;
1072
- }
1073
-
1074
- case 'affectedTests': {
1075
- const { ok, result, error, note } = execute(index, 'affectedTests', { name: arg, ...flags });
1076
- if (!ok) fail(error);
1077
- printOutput(result, output.formatAffectedTestsJson, r => output.formatAffectedTests(r, { all: flags.all }));
1078
- if (note) console.error(note);
1079
- break;
1080
- }
1081
-
1082
- case 'api': {
1083
- const filePath = arg || flags.file;
1084
- const { ok, result, error, note } = execute(index, 'api', { file: filePath, limit: flags.limit });
1085
- if (!ok) fail(error);
1086
- if (note) console.error(note);
1087
- printOutput(result,
1088
- r => output.formatApiJson(r, filePath),
1089
- r => output.formatApi(r, filePath)
1090
- );
1091
- break;
1092
- }
1093
-
1094
- case 'search': {
1095
- const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...flags });
1096
- if (!ok) fail(error);
1097
- if (structural) {
1098
- printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
1099
- } else {
1100
- printOutput(result,
1101
- r => output.formatSearchJson(r, arg),
1102
- r => output.formatSearch(r, arg)
1103
- );
1104
- }
1105
- break;
1106
- }
1107
-
1108
- case 'deadcode': {
1109
- const { ok, result, error, note } = execute(index, 'deadcode', { ...flags, in: flags.in || subdirScope });
1110
- if (!ok) fail(error);
1111
- if (note) console.error(note);
1112
- printOutput(result,
1113
- output.formatDeadcodeJson,
1114
- r => output.formatDeadcode(r, {
1115
- top: flags.top,
1116
- decoratedHint: !flags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
1117
- exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
1118
- externalContractHint: !flags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
1119
- })
1120
- );
1121
- break;
1122
- }
1123
-
1124
- case 'entrypoints': {
1125
- 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 });
1126
- if (!ok) fail(error);
1127
- if (note) console.error(note);
1128
- printOutput(result,
1129
- output.formatEntrypointsJson,
1130
- r => output.formatEntrypoints(r)
1131
- );
1132
- break;
1133
- }
1134
-
1135
- case 'endpoints': {
1136
- const { ok, result, error, note } = execute(index, 'endpoints', {
1137
- file: flags.file,
1138
- exclude: flags.exclude,
1139
- limit: flags.limit,
1140
- framework: flags.framework,
1141
- bridge: flags.bridge,
1142
- serverOnly: flags.serverOnly,
1143
- clientOnly: flags.clientOnly,
1144
- unmatched: flags.unmatched,
1145
- method: flags.method,
1146
- prefix: flags.prefix,
1147
- hideUncertain: flags.hideUncertain,
1148
- });
1149
- if (!ok) fail(error);
1150
- if (note) console.error(note);
1151
- printOutput(result,
1152
- output.formatEndpointsJson,
1153
- r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched })
1154
- );
1155
- break;
1156
- }
1157
-
1158
- case 'stats': {
1159
- // MEDIUM-7: pass the raw --top value when present so the executor
1160
- // can validate it and surface "Invalid --top" errors. Without
1161
- // this, --top=abc is silently coerced to NaN → undefined and
1162
- // the user gets the default (10) with no warning.
1163
- const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
1164
- const { ok, result, error, note } = execute(index, 'stats', {
1165
- functions: flags.functions,
1166
- hot: flags.hot,
1167
- top: topVal,
1168
- });
1169
- if (!ok) fail(error);
1170
- if (note) console.error(note);
1171
- printOutput(result,
1172
- output.formatStatsJson,
1173
- r => output.formatStats(r, { top: flags.top })
1174
- );
1175
- break;
1176
- }
1177
-
1178
- case 'diffImpact': {
1179
- const { ok, result, error, note } = execute(index, 'diffImpact', { base: flags.base, staged: flags.staged, file: flags.file, limit: flags.limit, all: flags.all });
1180
- if (!ok) fail(error);
1181
- if (note) console.error(note);
1182
- printOutput(result, output.formatDiffImpactJson, r => output.formatDiffImpact(r, { all: flags.all }));
1183
- break;
1184
- }
1185
-
1186
- case 'auditAsync': {
1187
- const { ok, result, error, note } = execute(index, 'auditAsync', {
1188
- file: flags.file,
1189
- exclude: flags.exclude,
1190
- limit: flags.limit,
1191
- });
1192
- if (!ok) fail(error);
1193
- if (note) console.error(note);
1194
- printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
1195
- break;
1196
- }
1197
-
1198
- default:
1199
- console.error(`Unknown command: ${canonical}`);
1200
- printUsage();
1201
- throw new CommandError();
731
+ // Public commands share one argument builder, executor, and formatter.
732
+ const publicParams = buildPublicParams(canonical, arg, {
733
+ ...flags,
734
+ ...(subdirScope && !flags.in ? { in: subdirScope } : {}),
735
+ });
736
+ const publicExecution = execute(index, canonical, publicParams);
737
+ if (!publicExecution.ok) {
738
+ fail(formatSurfaceMessage(publicExecution.error, 'cli'));
1202
739
  }
740
+ console.log(flags.json
741
+ ? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
742
+ ...publicExecution, surface: 'cli',
743
+ })
744
+ : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
745
+ // A gate that could not run (check outside git / bad base ref) must not
746
+ // exit 0 — CI gating on the exit code would read "could not run" as "passed".
747
+ process.exitCode = Math.max(process.exitCode || 0,
748
+ resultExitCode(canonical, publicExecution.result));
1203
749
  } catch (e) {
1204
750
  if (!(e instanceof CommandError)) {
1205
- console.error(`Error: ${e.message}`);
751
+ emitCliError(`Error: ${e.message}`);
1206
752
  }
1207
753
  process.exitCode = 1;
1208
754
  } finally {
@@ -1211,57 +757,12 @@ function runProjectCommand(rootDir, command, arg) {
1211
757
  // On cache-hit runs, only re-save if callsCache was mutated OR
1212
758
  // reachability was computed (MED-1: persists the BFS result so
1213
759
  // subsequent cold invocations don't repeat the 7-11s tax).
1214
- if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty)) {
760
+ if (flags.cache && (needsCacheSave || index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
1215
761
  try { index.saveCache(); } catch (e) { /* best-effort */ }
1216
762
  }
1217
763
  }
1218
764
  }
1219
765
 
1220
- // extractFunctionFromProject and extractClassFromProject removed —
1221
- // all surfaces now use execute(index, 'fn'/'class', params) from core/execute.js
1222
-
1223
-
1224
- /**
1225
- * Save expandable items to cache file
1226
- */
1227
- function saveExpandableItems(items, root) {
1228
- try {
1229
- const cacheDir = path.join(root || '.', '.ucn-cache');
1230
- if (!fs.existsSync(cacheDir)) {
1231
- fs.mkdirSync(cacheDir, { recursive: true });
1232
- }
1233
- fs.writeFileSync(
1234
- path.join(cacheDir, 'expandable.json'),
1235
- JSON.stringify({ items, root, timestamp: Date.now() }, null, 2)
1236
- );
1237
- } catch (e) {
1238
- // Silently fail - expand feature is optional
1239
- }
1240
- }
1241
-
1242
- /**
1243
- * Load expandable items from cache
1244
- */
1245
- function loadExpandableItems(root) {
1246
- try {
1247
- const cachePath = path.join(root || '.', '.ucn-cache', 'expandable.json');
1248
- if (fs.existsSync(cachePath)) {
1249
- return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
1250
- }
1251
- } catch (e) {
1252
- // Return null on error
1253
- }
1254
- return null;
1255
- }
1256
-
1257
- /**
1258
- * Print expanded code for a cached item
1259
- */
1260
- // printExpandedItem removed — all surfaces now use execute(index, 'expand', ...)
1261
-
1262
-
1263
-
1264
-
1265
766
  // ============================================================================
1266
767
  // GLOB MODE
1267
768
  // ============================================================================
@@ -1270,11 +771,11 @@ function runGlobCommand(pattern, command, arg) {
1270
771
  const files = expandGlob(pattern);
1271
772
 
1272
773
  if (files.length === 0) {
1273
- console.error(`No files match pattern: ${pattern}`);
1274
- process.exit(1);
774
+ fail(`No files match pattern: ${pattern}`, command);
1275
775
  }
1276
776
 
1277
777
  const canonical = resolveCommand(command, 'cli') || command;
778
+ activeCanonicalCommand = canonical;
1278
779
 
1279
780
  // Build a temporary index over the matched files and route through execute().
1280
781
  // This gives glob mode the same semantics as project mode: test exclusions,
@@ -1283,223 +784,22 @@ function runGlobCommand(pattern, command, arg) {
1283
784
  const index = new ProjectIndex(rootDir);
1284
785
  index.build(files, { quiet: true });
1285
786
 
1286
- // Supported commands — anything that works with an index.
1287
- // All execute() commands are supported; only expand (requires cached state)
1288
- // and interactive-only commands are excluded.
1289
- const unsupportedGlobCommands = new Set(['expand']);
1290
- if (unsupportedGlobCommands.has(canonical)) {
1291
- console.error(`Command "${command}" not supported in glob mode.`);
1292
- process.exit(1);
1293
- }
1294
-
1295
- // Build params — same as project mode
1296
- const params = {};
1297
- const needsName = new Set(['find', 'usages', 'fn', 'class', 'typedef', 'about', 'context',
1298
- 'smart', 'impact', 'trace', 'blast', 'reverseTrace', 'tests', 'affectedTests',
1299
- 'example', 'verify', 'plan', 'related']);
1300
- if (needsName.has(canonical)) {
1301
- if (!arg) {
1302
- console.error(`Usage: ucn "pattern" ${command} <name>`);
1303
- process.exit(1);
1304
- }
1305
- params.name = arg;
1306
- }
1307
- if (canonical === 'search' || canonical === 'structuralSearch') {
1308
- if (!arg && !flags.type) {
1309
- console.error('Usage: ucn "pattern" search <term>');
1310
- process.exit(1);
1311
- }
1312
- params.term = arg;
1313
- }
1314
- // Merge flags first, then set positional overrides so they aren't wiped
1315
- Object.assign(params, flags);
1316
-
1317
- // Warn about inapplicable flags (same check as project/interactive mode)
1318
- const applicableFlags = FLAG_APPLICABILITY[canonical];
1319
- if (applicableFlags) {
1320
- const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
1321
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
1322
- for (const [key, value] of Object.entries(flags)) {
1323
- if (globalFlags.has(key)) continue;
1324
- if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
1325
- if (!applicableFlags.includes(key)) {
1326
- console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
1327
- }
1328
- }
1329
- }
1330
- if (canonical === 'stacktrace' && arg) {
1331
- params.stack = arg;
787
+ if (!isPublicCommand(canonical)) {
788
+ fail(unknownCommandMessage(command));
1332
789
  }
1333
- if (canonical === 'lines' && arg) {
1334
- params.range = arg;
1335
- }
1336
- if (['imports', 'exporters', 'fileExports', 'graph', 'api'].includes(canonical)) {
1337
- if (arg) params.file = arg;
1338
- }
1339
-
1340
- const { ok, result, error, note, structural } = execute(index, canonical, params);
1341
- if (!ok) fail(error);
1342
- if (note) console.error(note);
1343
-
1344
- // Format output — same formatters as project mode
1345
- switch (canonical) {
1346
- case 'toc':
1347
- printOutput(result, output.formatTocJson, r => output.formatToc(r, {
1348
- detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
1349
- }));
1350
- break;
1351
- case 'find':
1352
- printOutput(result,
1353
- r => output.formatSymbolJson(r, arg),
1354
- r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
1355
- );
1356
- break;
1357
- case 'search':
1358
- if (structural) {
1359
- printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
1360
- } else {
1361
- printOutput(result,
1362
- r => output.formatSearchJson(r, arg),
1363
- r => output.formatSearch(r, arg)
1364
- );
1365
- }
1366
- break;
1367
- case 'fn':
1368
- printOutput(result, output.formatFnResultJson, output.formatFnResult);
1369
- break;
1370
- case 'class':
1371
- printOutput(result, output.formatClassResultJson, output.formatClassResult);
1372
- break;
1373
- case 'usages':
1374
- printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
1375
- break;
1376
- case 'deadcode':
1377
- printOutput(result, output.formatDeadcodeJson, r => output.formatDeadcode(r, { top: flags.top }));
1378
- break;
1379
- case 'typedef':
1380
- printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
1381
- break;
1382
- case 'stats':
1383
- printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
1384
- break;
1385
- case 'about':
1386
- printOutput(result, output.formatAboutJson,
1387
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false, compact: !!flags.compact }));
1388
- break;
1389
- case 'context':
1390
- if (flags.json) {
1391
- console.log(output.formatContextJson(result));
1392
- } else {
1393
- const { text } = output.formatContext(result, {
1394
- expandHint: 'Use --expand to see inline callee previews',
1395
- showConfidence: flags.showConfidence !== false,
1396
- compact: !!flags.compact,
1397
- });
1398
- console.log(text);
1399
- if (flags.expand) {
1400
- printInlineExpand(result, index.root);
1401
- }
1402
- }
1403
- break;
1404
- case 'smart':
1405
- printOutput(result, output.formatSmartJson, output.formatSmart);
1406
- break;
1407
- case 'impact':
1408
- printOutput(result, output.formatImpactJson, output.formatImpact);
1409
- break;
1410
- case 'related':
1411
- printOutput(result, output.formatRelatedJson,
1412
- r => output.formatRelated(r, { all: flags.all, top: flags.top }));
1413
- break;
1414
- case 'brief':
1415
- printOutput(result, output.formatBriefJson, output.formatBrief);
1416
- break;
1417
- case 'doctor':
1418
- printOutput(result, output.formatDoctorJson, output.formatDoctor);
1419
- break;
1420
- case 'check':
1421
- printOutput(result, output.formatCheckJson, output.formatCheck);
1422
- break;
1423
- case 'trace':
1424
- printOutput(result, output.formatTraceJson, output.formatTrace);
1425
- break;
1426
- case 'blast':
1427
- printOutput(result, output.formatBlastJson, output.formatBlast);
1428
- break;
1429
- case 'reverseTrace':
1430
- printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
1431
- break;
1432
- case 'tests':
1433
- printOutput(result, r => output.formatTestsJson(r, arg), r => output.formatTests(r, arg));
1434
- break;
1435
- case 'affectedTests':
1436
- printOutput(result, output.formatAffectedTestsJson,
1437
- r => output.formatAffectedTests(r, { all: flags.all }));
1438
- break;
1439
- case 'example':
1440
- printOutput(result, r => output.formatExampleJson(r, arg), r => output.formatExample(r, arg));
1441
- break;
1442
- case 'verify':
1443
- printOutput(result, output.formatVerifyJson, output.formatVerify);
1444
- break;
1445
- case 'plan':
1446
- printOutput(result, output.formatPlanJson, output.formatPlan);
1447
- break;
1448
- case 'imports': {
1449
- const filePath = params.file;
1450
- printOutput(result, r => output.formatImportsJson(r, filePath), r => output.formatImports(r, filePath));
1451
- break;
1452
- }
1453
- case 'exporters': {
1454
- const filePath = params.file;
1455
- printOutput(result, r => output.formatExportersJson(r, filePath), r => output.formatExporters(r, filePath));
1456
- break;
1457
- }
1458
- case 'fileExports': {
1459
- const filePath = params.file;
1460
- printOutput(result, r => output.formatFileExportsJson(r, filePath), r => output.formatFileExports(r, filePath));
1461
- break;
1462
- }
1463
- case 'api': {
1464
- const filePath = params.file;
1465
- printOutput(result, r => output.formatApiJson(r, filePath), r => output.formatApi(r, filePath));
1466
- break;
1467
- }
1468
- case 'graph':
1469
- printOutput(result, output.formatGraphJson,
1470
- r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth }));
1471
- break;
1472
- case 'circularDeps':
1473
- printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
1474
- break;
1475
- case 'entrypoints':
1476
- printOutput(result, output.formatEntrypointsJson, output.formatEntrypoints);
1477
- break;
1478
- case 'endpoints':
1479
- printOutput(result, output.formatEndpointsJson, r => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }));
1480
- break;
1481
- case 'diffImpact':
1482
- printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
1483
- break;
1484
- case 'auditAsync':
1485
- printOutput(result, output.formatAuditAsyncJson, output.formatAuditAsync);
1486
- break;
1487
- case 'stacktrace':
1488
- printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
1489
- break;
1490
- case 'lines':
1491
- printOutput(result, output.formatLinesJson, output.formatLines);
1492
- break;
1493
- default: {
1494
- // Fallback: output JSON for any command without a dedicated formatter
1495
- if (flags.json) {
1496
- console.log(JSON.stringify({ meta: {}, data: result }, null, 2));
1497
- } else {
1498
- console.log(JSON.stringify(result, null, 2));
1499
- }
1500
- break;
1501
- }
790
+ warnInapplicableFlags(canonical, flags, (message) => console.error(message));
791
+ const publicParams = buildPublicParams(canonical, arg, flags);
792
+ const publicExecution = execute(index, canonical, publicParams);
793
+ if (!publicExecution.ok) {
794
+ fail(formatSurfaceMessage(publicExecution.error, 'cli'));
1502
795
  }
796
+ console.log(flags.json
797
+ ? output.formatPublicJson(canonical, publicExecution.result, publicParams, {
798
+ ...publicExecution, surface: 'cli',
799
+ })
800
+ : formatCliText(canonical, publicExecution.result, publicParams, publicExecution, flags));
801
+ process.exitCode = Math.max(process.exitCode || 0,
802
+ resultExitCode(canonical, publicExecution.result));
1503
803
  }
1504
804
 
1505
805
  // ============================================================================
@@ -1510,153 +810,93 @@ function runGlobCommand(pattern, command, arg) {
1510
810
  // Single source of truth for the public CLI help. README points here ("Run `ucn --help`")
1511
811
  // rather than carrying a copy — keep it that way.
1512
812
  function printUsage() {
813
+ const perCommandFlags = [...getCliCommandSet()].map(command => {
814
+ const flags = getCliFlagsForCommand(command);
815
+ return ` ${command.padEnd(14)} ${flags.join(' ')}`;
816
+ }).join('\n');
1513
817
  console.log(`UCN - Universal Code Navigator
1514
818
 
1515
- Supported: JavaScript, TypeScript, Python, Go, Rust, Java, HTML
819
+ Supported: JavaScript/TypeScript, Python, Go, Rust, Java, C, C++, C#, HTML
1516
820
 
1517
821
  Usage:
1518
- ucn [command] [args] Project mode (current directory)
1519
- ucn <file> [command] [args] Single file mode
1520
- ucn <dir> [command] [args] Project mode (specific directory)
1521
- ucn "pattern" [command] [args] Glob pattern mode
1522
- (Default output is text; add --json for machine-readable JSON)
1523
-
1524
- ═══════════════════════════════════════════════════════════════════════════════
1525
- UNDERSTAND CODE
1526
- ═══════════════════════════════════════════════════════════════════════════════
1527
- about <name> Full picture (definition, callers, callees, tests, code)
1528
- brief <name> One-screen summary (signature, docstring, side effects, complexity)
1529
- context <name> Who calls this + what it calls (numbered for expand)
1530
- smart <name> Function + all dependencies inline
1531
- impact <name> What breaks if changed (call sites grouped by file)
1532
- blast <name> Transitive blast radius (callers of callers, --depth=N)
1533
- trace <name> Call tree visualization (--depth=N expands all children)
1534
- reverse-trace <name> Upward call chain to entry points (--depth=N, default 5)
1535
- related <name> Find similar functions (same file, shared deps)
1536
- example <name> Best usage example with context
1537
-
1538
- ═══════════════════════════════════════════════════════════════════════════════
1539
- FIND CODE
1540
- ═══════════════════════════════════════════════════════════════════════════════
1541
- find <name> Find symbol definitions (supports glob: find "handle*")
1542
- usages <name> All usages grouped: definitions, calls, imports, references
1543
- toc Table of contents (compact; --detailed lists all symbols)
1544
- search <term> Text search (regex default, --context=N, --exclude=, --in=)
1545
- Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
1546
- tests <name> Find test files for a function (--file, --class-name, --exclude, --calls-only)
1547
- affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
1548
-
1549
- ═══════════════════════════════════════════════════════════════════════════════
1550
- EXTRACT CODE
1551
- ═══════════════════════════════════════════════════════════════════════════════
1552
- fn <name>[,n2,...] Extract function(s) (comma-separated for bulk, --file)
1553
- class <name> Extract class
1554
- lines <range> Extract line range (e.g., lines 50-100)
1555
- expand <N> Show code for item N from context output
1556
-
1557
- ═══════════════════════════════════════════════════════════════════════════════
1558
- FILE DEPENDENCIES
1559
- ═══════════════════════════════════════════════════════════════════════════════
1560
- imports <file> What does file import
1561
- exporters <file> Who imports this file
1562
- file-exports <file> What does file export
1563
- graph <file> Full dependency tree (--depth=N, --direction=imports|importers|both)
1564
- circular-deps Detect circular import chains (--file=, --exclude=)
1565
-
1566
- ═══════════════════════════════════════════════════════════════════════════════
1567
- REFACTORING HELPERS
1568
- ═══════════════════════════════════════════════════════════════════════════════
1569
- plan <name> Preview refactoring (--add-param, --remove-param, --rename-to, --default-value)
1570
- verify <name> Check all call sites match signature
1571
- diff-impact What changed in git diff and who calls it (--base, --staged)
1572
- check Pre-commit summary: diff-impact + verify + affected-tests in one shot
1573
- deadcode Unreferenced-symbol candidates (review before deletion)
1574
- entrypoints Detect framework entry points (routes, DI, tasks)
1575
- endpoints HTTP API: list server routes + client requests; --bridge to match
1576
- --bridge --server-only --client-only --unmatched
1577
- --method=GET --prefix=/api --hide-uncertain
1578
-
1579
- ═══════════════════════════════════════════════════════════════════════════════
1580
- OTHER
1581
- ═══════════════════════════════════════════════════════════════════════════════
1582
- api Show exported/public symbols
1583
- typedef <name> Find type definitions
1584
- stats Project statistics (--functions for per-function line counts, --hot for top callers)
1585
- doctor Parse health, blind spots, command proofs, and task readiness (--deep adds evidence profile)
1586
- orient One-screen repo map: size, top dirs, hot functions, entry points, readiness (--top=N)
1587
- stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
1588
- audit-async Find calls in async functions that are likely missing await (JS/TS/Python)
1589
-
1590
- Common Flags:
1591
- --file <pattern> Filter by file path (e.g., --file=routes)
1592
- --exclude=a,b Exclude patterns (e.g., --exclude=test,mock)
1593
- --in=<path> Only in path (e.g., --in=src/core)
1594
- --depth=N Max depth: blast=3, trace=3, reverse-trace=5, graph=2, affected-tests=3
1595
- --direction=X Graph direction: imports, importers, or both (default: both)
1596
- --all Show full results: all callers/callees + unverified (about/context), full tree (trace/blast),
1597
- all names (related/find/fn/class/toc), all changed (diff-impact)
1598
- --top=N Limit callers/callees (about), similar functions (related), search results
1599
- --limit=N Limit result count (find, usages, search, deadcode, api, toc, entrypoints, diff-impact)
1600
- --max-files=N Max files to index (large projects)
1601
- --context=N Lines of context around matches (search, usages)
1602
- --json Machine-readable output
1603
- --compact Token-efficient about/context/impact output
1604
- --code-only Filter out comments/strings (search, usages)
1605
- --with-types Include type definitions (about, smart)
1606
- --detailed Show all symbols in toc (not just counts)
1607
- --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1608
- --exclude-tests Exclude test files (entrypoints includes tests by default)
1609
- --class-name=X Scope to specific class (e.g., --class-name=Repository)
1610
- --include-methods Include method-call (obj.fn) callee expansion in trace/smart
1611
- (no effect on caller-direction commands: about/context/impact/verify/
1612
- blast/reverse-trace/affected-tests always tier method calls by evidence)
1613
- --include-uncertain No effect on tiered commands; unverified candidates are always
1614
- shown in their own section with reasons
1615
- --expand-unverified Follow unverified caller edges in blast/reverse-trace trees
1616
- (downstream nodes marked as possible, not confirmed, impact chains)
1617
- --hide-confidence Hide confidence scores (shown by default in about, context)
1618
- --min-confidence=N Filter low-confidence edges (about, context, blast, trace,
1619
- reverse-trace, smart, affected-tests)
1620
- --unreachable-only Show only callers/callees that are unreachable from entry points (about, context, impact)
1621
- --include-exported Include exported symbols in deadcode
1622
- --no-regex Force plain text search (regex is default)
1623
- --functions Show per-function line counts (stats command)
1624
- --hot Show top N most-called functions (stats command, pair with --top=N)
1625
- --diverse Cluster call sites by argument shape (example command, pair with --top=N)
1626
- --git Attach git enrichment (last modified, author, recent commits) to about/brief
1627
- --include-decorated Include decorated/annotated symbols in deadcode
1628
- --deep Add a stratified evidence profile to doctor (not measured accuracy)
1629
- --framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
1630
- --bridge Match server routes to client requests (endpoints command).
1631
- Confidence tiers: EXACT, PARTIAL, UNCERTAIN
1632
- --server-only Only list server routes (endpoints command)
1633
- --client-only Only list client requests (endpoints command)
1634
- --unmatched Only show routes/requests with no match (endpoints, pair with --bridge)
1635
- --method=X Filter by HTTP method (endpoints, e.g., --method=POST)
1636
- --prefix=X Filter routes/requests by path prefix (endpoints, e.g., --prefix=/api)
1637
- --hide-uncertain Hide UNCERTAIN-confidence bridges (endpoints command)
1638
- --exact Exact name match only (find, typedef)
1639
- --calls-only Only show call/test-case matches (tests)
1640
- --case-sensitive Case-sensitive text search (search)
1641
- --top-level Show only top-level functions in toc
1642
- --max-lines=N Max source lines for class (large classes show summary)
1643
- --workers=N Parallel build workers (auto-detect; 0 to disable, env: UCN_WORKERS)
1644
- --no-cache Disable caching
1645
- --clear-cache Clear cache before running
1646
- --base=<ref> Git ref for diff-impact (default: HEAD)
1647
- --staged Analyze staged changes (diff-impact)
1648
- --no-follow-symlinks Don't follow symbolic links
1649
- --mcp Start the MCP stdio server
1650
- -i, --interactive Keep index in memory for multiple queries
1651
- -v, --version Print the UCN version and exit
1652
-
1653
- Quick Start:
1654
- ucn orient # First look at a new repo
1655
- ucn toc # See project structure
1656
- ucn about handleRequest # Understand a function
1657
- ucn impact handleRequest # Before modifying
1658
- ucn fn handleRequest --file api # Extract specific function
1659
- ucn --interactive # Multiple queries`);
822
+ ucn [command] [args] Current project
823
+ ucn <file|dir|glob> <command> Explicit target
824
+ Add --json for a stable { meta, data } envelope.
825
+
826
+ Commands:
827
+ repo Repository overview
828
+ --sections=summary,files,stats,health Select repository sections
829
+ --deep Include deep readiness evidence
830
+ show <symbol> Symbol summary and relationships
831
+ --sections=summary,callers,callees,source,dependencies,tests,types,example,related
832
+ find <name> Definitions; --type=type, --with-source
833
+ usages <name> Calls, imports, definitions, references
834
+ search [term] Text or structural search
835
+ literal text by default; --regex enables regular-expression syntax
836
+ source <symbol|file:range> Exact function, class, or line extraction
837
+ trace <symbol> Call graph
838
+ --direction=callees|callers Downstream or upstream (default: callees)
839
+ --to=entrypoints Follow callers toward entry points
840
+ impact [symbol] Symbol impact; without symbol, Git-diff impact
841
+ tests <symbol> Direct tests; --depth=N adds transitive impact
842
+ deps <file> File graph; --direction=imports|importers|both
843
+ --detailed Include import declarations
844
+ deps --cycles Report circular dependencies (no file target)
845
+ api [file] Project or file public API
846
+ check [symbol] Signature check; without symbol, precommit check
847
+ plan <symbol> Preview rename or parameter edits
848
+ entrypoints Runtime and framework entry points
849
+ endpoints Server/client HTTP surface
850
+ deadcode Conservative unreachable-symbol candidates
851
+ audit-async Likely missing-await sites
852
+ stacktrace <text> Resolve runtime frames to source
853
+
854
+ Common flags:
855
+ --file=PATH --exclude=a,b --in=PATH --depth=N --top=N --limit=N
856
+ --all --compact --no-compact --json --include-tests --class-name=X --line=N
857
+ --range=N-M (source with --file=PATH)
858
+ --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
859
+ --max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
860
+ Cache: per-user by default; set UCN_CACHE_DIR to override the cache root.
861
+
862
+ Accepted flags by command:
863
+ ${perCommandFlags}
864
+
865
+ Global/build/output flags:
866
+ --help -h --version -v --mcp --json --verbose --no-quiet --quiet
867
+ --interactive -i --no-cache --clear-cache --no-follow-symlinks
868
+ --max-files=N --max-chars=N --workers=N
869
+ --clear-cache --all clears every bounded per-user UCN project cache.
870
+
871
+ Exit codes:
872
+ 0 Command completed successfully; check found no blocking issues.
873
+ 1 Findings, unsafe changes, validation failures, or invalid user input.
874
+ 2 Command could not run (operational/environment failure).
875
+
876
+ Boolean aliases:
877
+ --no-include-methods --no-regex --show-confidence --hide-confidence
878
+ --no-confidence --hide-uncertain --no-uncertain --compact --no-compact
879
+
880
+ Value aliases:
881
+ --not=PATTERN (alias of --exclude) --default=VALUE (alias of --default-value)
882
+
883
+ Trust:
884
+ CONFIRMED means target-identity evidence exists. UNVERIFIED means possible and
885
+ requires review. ACCOUNT conserves observed text lines; it never proves full
886
+ runtime semantics or safe deletion.
887
+
888
+ UCN vs grep:
889
+ Use UCN for definitions, callers/callees, impact, tests, dependencies, APIs,
890
+ entry points, and audits. Use grep/ripgrep for simple literals, messages,
891
+ configuration, filenames, Markdown, and unsupported languages.
892
+
893
+ Quick start:
894
+ ucn repo
895
+ ucn show handleRequest
896
+ ucn trace handleRequest --direction=callers
897
+ ucn impact handleRequest
898
+ ucn tests handleRequest --depth=3
899
+ ucn source handleRequest`);
1660
900
  }
1661
901
 
1662
902
  // ============================================================================
@@ -1669,12 +909,18 @@ function runInteractive(rootDir) {
1669
909
 
1670
910
  console.log('Building index...');
1671
911
  const index = new ProjectIndex(rootDir);
912
+ if (flags.clearCache) {
913
+ const removed = index.clearCache();
914
+ if (removed.length > 0 && !flags.quiet) {
915
+ console.error('Cache cleared');
916
+ }
917
+ }
1672
918
  // Same cache discipline as one-shot mode (fix #250: the REPL fully
1673
919
  // re-parsed every session and never consumed cache-persisted state —
1674
920
  // the divergence mechanism behind the relocation P1).
1675
921
  let iCacheFresh;
1676
- if (flags.cache && !flags.clearCache) {
1677
- const loaded = index.loadCache();
922
+ if (flags.cache) {
923
+ const loaded = !flags.clearCache && index.loadCache();
1678
924
  iCacheFresh = loaded && !index.isCacheStale();
1679
925
  if (!iCacheFresh && loaded) {
1680
926
  index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
@@ -1687,9 +933,8 @@ function runInteractive(rootDir) {
1687
933
  } else {
1688
934
  index.build(null, { quiet: true, workers: flags.workers });
1689
935
  }
1690
- const iExpandCache = new ExpandCache({ maxSize: 20 });
1691
936
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1692
- console.log('Type commands (e.g., "find parseFile", "about main", "toc")');
937
+ console.log('Type commands (e.g., "find parseFile", "show main", "repo")');
1693
938
  console.log('Type "help" for commands, "quit" to exit\n');
1694
939
 
1695
940
  const rl = readline.createInterface({
@@ -1716,50 +961,29 @@ function runInteractive(rootDir) {
1716
961
  if (input === 'help') {
1717
962
  console.log(`
1718
963
  Commands:
1719
- toc Project overview (--detailed)
1720
- find <name> Find symbol (--exact, glob: "handle*")
1721
- brief <name> Signature, docs, side effects, and complexity
1722
- about <name> Everything about a symbol
964
+ repo Repository overview (--sections=files,stats,health)
965
+ show <name> Symbol summary + relationships (--sections=...)
966
+ find <name> Find definitions (--type=type, --with-source)
1723
967
  usages <name> All usages grouped by type
1724
- context <name> Callers + callees
1725
- expand <N> Show code for item N from context
1726
- smart <name> Function + dependencies
1727
- impact <name> What breaks if changed
1728
- blast <name> Transitive blast radius (--depth=N)
1729
- trace <name> Call tree (--depth=N)
1730
- reverse-trace <name> Upward to entry points (--depth=N)
1731
- example <name> Best usage example
1732
- related <name> Sibling functions
1733
- fn <name>[,n2,...] Extract function(s) (--file=)
1734
- class <name> Extract class code (--file=)
1735
- lines <range> Extract lines (--file= required)
1736
- graph <file> File dependency tree (--direction=, --depth=)
1737
- circular-deps Circular import chains (--file=, --exclude=)
1738
- file-exports <file> File's exported symbols
1739
- imports <file> What file imports
1740
- exporters <file> Who imports file
1741
- tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
1742
- affected-tests <n> Tests affected by a change (--depth=N)
968
+ source <target> Extract a symbol or file:line-range
969
+ trace <name> Call tree (--direction=callees|callers, --to=entrypoints)
970
+ impact [name] Symbol impact, or Git diff impact without a name
971
+ tests <name> Direct tests; --depth=N includes transitive impact
972
+ deps <file> File graph (--direction=, --depth=, --detailed, --cycles)
1743
973
  search <term> Text search (--context=N, --exclude=, --in=)
1744
974
  Structural: --type= --param= --returns= --decorator= --exported --unused
1745
- typedef <name> Find type definitions
1746
975
  deadcode Unreferenced-symbol candidates (review before deletion)
1747
976
  entrypoints Detect runtime, framework, task, and test entry points
1748
977
  endpoints List server/client HTTP endpoints (--bridge to match)
1749
- verify <name> Check call sites match signature
978
+ check [name] Symbol signature check, or pre-commit check without a name
1750
979
  plan <name> Preview refactoring (--add-param=, --remove-param=, --rename-to=, --default-value=)
1751
- check Pre-commit diff, signature, and affected-test checks
1752
980
  stacktrace <text> Parse a stack trace
1753
981
  api Show public symbols
1754
- diff-impact What changed and who's affected
1755
- stats Index statistics
1756
- doctor Parse health, blind spots, command proofs, and task readiness
1757
- orient Repository map and readiness summary
1758
- audit-async Find likely missing-await calls (JS/TS/Python)
982
+ audit-async Find likely missing-await calls (JS/TS/Python/C#)
1759
983
  rebuild Rebuild index
1760
984
  quit Exit
1761
985
 
1762
- Flags can be added per-command: context myFunc --include-methods
986
+ Flags can be added per-command: show myFunc --sections=source,callers
1763
987
  `);
1764
988
  rl.prompt();
1765
989
  return;
@@ -1768,8 +992,6 @@ Flags can be added per-command: context myFunc --include-methods
1768
992
  if (input === 'rebuild') {
1769
993
  console.log('Rebuilding index...');
1770
994
  index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
1771
- // Clear expand cache — stale line ranges after rebuild
1772
- if (iExpandCache) iExpandCache.clearForRoot(index.root);
1773
995
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1774
996
  rl.prompt();
1775
997
  return;
@@ -1779,7 +1001,7 @@ Flags can be added per-command: context myFunc --include-methods
1779
1001
  const tokens = input.split(/\s+/);
1780
1002
  const command = tokens[0];
1781
1003
  // Flags that take a space-separated value (--flag value)
1782
- 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', '--line', '--framework', '--method', '--prefix']);
1004
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--to', '--sections', '--range', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--max-chars', '--min-confidence', '--class-name', '--line', '--framework', '--method', '--prefix']);
1783
1005
  const flagTokens = [];
1784
1006
  const argTokens = [];
1785
1007
  const skipNext = new Set();
@@ -1798,9 +1020,8 @@ Flags can be added per-command: context myFunc --include-methods
1798
1020
  }
1799
1021
  const arg = argTokens.join(' ');
1800
1022
 
1801
- // Unknown flags error instead of folding their VALUE into the
1802
- // symbol name (fix #250: `about AddTask --bogus 5` searched for
1803
- // "AddTask 5"). Same vocabulary as one-shot mode.
1023
+ // Unknown flags error instead of folding their value into the symbol
1024
+ // name. Same vocabulary as one-shot mode.
1804
1025
  const unknown = flagTokens.filter(t =>
1805
1026
  t.startsWith('--') && !knownFlags.has(t.split('=')[0]));
1806
1027
  if (unknown.length > 0) {
@@ -1820,8 +1041,28 @@ Flags can be added per-command: context myFunc --include-methods
1820
1041
  // global CLI mode. MED-2/MED-3/MED-5: bad values are rejected with
1821
1042
  // a helpful message instead of being silently coerced.
1822
1043
  validateNumericFlags(iflags);
1044
+ // The REPL is an edit/query loop, not a frozen snapshot. Keep the
1045
+ // symbol table and call cache on the same generation by checking
1046
+ // source staleness before every command. getCachedCalls already
1047
+ // refreshes individual call records lazily; without this matching
1048
+ // rebuild a newly added definition was invisible while its calls
1049
+ // appeared in neighbouring answers (UCN5-044).
1050
+ if (index.isCacheStale()) {
1051
+ console.log('Source changed; rebuilding index...');
1052
+ index.build(null, {
1053
+ quiet: true,
1054
+ forceRebuild: true,
1055
+ followSymlinks: flags.followSymlinks,
1056
+ maxFiles: flags.maxFiles,
1057
+ workers: flags.workers,
1058
+ });
1059
+ if (flags.cache) {
1060
+ try { index.saveCache(); } catch (_) { /* best-effort */ }
1061
+ }
1062
+ console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1063
+ }
1823
1064
  const iCanonical = resolveCommand(command, 'cli') || command;
1824
- executeInteractiveCommand(index, iCanonical, arg, iflags, iExpandCache);
1065
+ executeInteractiveCommand(index, iCanonical, arg, iflags);
1825
1066
  } catch (e) {
1826
1067
  if (e instanceof FlagValidationError) {
1827
1068
  console.log(e.message);
@@ -1838,222 +1079,26 @@ Flags can be added per-command: context myFunc --include-methods
1838
1079
  });
1839
1080
  }
1840
1081
 
1841
- // parseInteractiveFlags removed — both global and interactive mode now use parseFlags()
1842
-
1843
- // ── Data-driven interactive command dispatch ─────────────────────────────
1844
- //
1845
- // Each entry maps a canonical command name to:
1846
- // params: (arg, iflags) => execute() params object
1847
- // format: (result, arg, iflags, index) => formatted string
1848
- //
1849
- // The generic handler calls execute(), checks errors, prints notes, and
1850
- // formats the result. Only commands with truly unique behavior (expand
1851
- // cache save, file writing, conditional formatters) keep explicit cases.
1852
-
1853
- const INTERACTIVE_DISPATCH = {
1854
- // ── Understanding Code ───────────────────────────────────────────
1855
- 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, compact: !!f.compact, git: !!f.git }) },
1856
- smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'unverified callees are listed below with reasons' }) },
1857
- impact: { params: 'name', format: (r, _a, f) => output.formatImpact(r, { compact: !!f.compact }) },
1858
- blast: { params: 'name', format: (r) => output.formatBlast(r) },
1859
- trace: { params: 'name', format: (r) => output.formatTrace(r) },
1860
- reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
1861
- related: { params: 'name', format: (r, _a, f) => output.formatRelated(r, { all: f.all, top: f.top }) },
1862
- 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) },
1863
- brief: { params: 'name', format: (r) => output.formatBrief(r) },
1864
-
1865
- // ── Finding Code ─────────────────────────────────────────────────
1866
- find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
1867
- usages: { params: 'name', format: (r, a, f) => output.formatUsages(r, a, { compact: !!f.compact }) },
1868
- toc: { params: 'flags', format: (r) => output.formatToc(r, { detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol', uncertainHint: 'run "about <name>" for tiered detail on a specific symbol' }) },
1869
- tests: { params: 'name', format: (r, a) => output.formatTests(r, a) },
1870
- affectedTests: { params: 'name', format: (r, _a, f) => output.formatAffectedTests(r, { all: f.all }) },
1871
- typedef: { params: 'name', format: (r, a) => output.formatTypedef(r, a) },
1872
-
1873
- // ── File Dependencies ────────────────────────────────────────────
1874
- imports: { params: 'file', format: (r, a, f) => output.formatImports(r, a || f.file) },
1875
- exporters: { params: 'file', format: (r, a, f) => output.formatExporters(r, a || f.file) },
1876
- fileExports: { params: 'file', format: (r, a, f) => output.formatFileExports(r, a || f.file) },
1877
- graph: { params: (a, f) => ({ file: a || f.file, direction: f.direction, depth: f.depth, all: f.all }), format: (r, a, f) => { const d = f.depth ? parseInt(f.depth) : 2; return output.formatGraph(r, { showAll: f.all || !!f.depth, maxDepth: d, file: a || f.file }); } },
1878
- circularDeps: { params: (a, f) => ({ file: f.file, exclude: f.exclude }), format: (r) => output.formatCircularDeps(r) },
1879
-
1880
- // ── Refactoring Helpers ──────────────────────────────────────────
1881
- plan: { params: 'name', format: (r) => output.formatPlan(r) },
1882
- verify: { params: 'name', format: (r) => output.formatVerify(r) },
1883
- 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 }) },
1884
- check: { params: (a, f) => ({ base: f.base, staged: f.staged, file: f.file, limit: f.limit }), format: (r) => output.formatCheck(r) },
1885
- 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) },
1886
- 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 }) },
1887
-
1888
- // ── Other ────────────────────────────────────────────────────────
1889
- api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file) },
1890
- stacktrace: { params: (a, f) => ({ stack: f.stack || a }), format: (r) => output.formatStackTrace(r) },
1891
- doctor: { params: (a, f) => ({ file: f.file, in: f.in, limit: f.limit, deep: f.deep }), format: (r) => output.formatDoctor(r) },
1892
- orient: { params: (a, f) => ({ top: f.topRaw != null ? f.topRaw : (f.top || undefined) }), format: (r) => output.formatOrient(r) },
1893
- // MED-2: stats handler in execute.js rejects top<=0; without explicit
1894
- // coercion, parseFlags's `top: 0` default would surface as
1895
- // "Invalid --top value" on bare `stats`. Mirror the project-mode top
1896
- // coercion (topRaw when present, else undefined for default-10).
1897
- 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 }) },
1898
- auditAsync: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit }), format: (r) => output.formatAuditAsync(r) },
1899
- };
1900
-
1901
- /**
1902
- * Build execute() params from a dispatch entry's params descriptor.
1903
- * 'name' → { name: arg, ...iflags }
1904
- * 'file' → { file: arg }
1905
- * 'flags' → iflags (no arg)
1906
- * function → custom builder
1907
- */
1908
- function buildInteractiveParams(descriptor, arg, iflags) {
1909
- if (typeof descriptor === 'function') return descriptor(arg, iflags);
1910
- switch (descriptor) {
1911
- case 'name': return { name: arg, ...iflags };
1912
- case 'file': return { file: arg || iflags.file };
1913
- case 'flags': return iflags;
1914
- default: return { name: arg, ...iflags };
1915
- }
1916
- }
1082
+ function executeInteractiveCommand(index, command, arg, iflags = {}) {
1083
+ warnInapplicableFlags(command, iflags, (message) => console.log(message));
1917
1084
 
1918
- function executeInteractiveCommand(index, command, arg, iflags = {}, cache = null) {
1919
- // Warn about inapplicable flags (same check as project mode)
1920
- const applicableFlags = FLAG_APPLICABILITY[command];
1921
- if (applicableFlags) {
1922
- const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
1923
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode', 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'depthRaw', 'contextRaw', 'workersRaw']);
1924
- for (const [key, value] of Object.entries(iflags)) {
1925
- if (globalFlags.has(key)) continue;
1926
- if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
1927
- if (!applicableFlags.includes(key)) {
1928
- console.log(`Warning: ${flagToCli(key)} has no effect on '${command}'.`);
1929
- }
1930
- }
1931
- // Tiered-output contract notes (fix #250 — one-shot mode printed
1932
- // these; interactive silently accepted the flags).
1933
- printTieredNoOpNotes(command, iflags, (m) => console.log(m));
1085
+ if (!isPublicCommand(command)) {
1086
+ console.log(unknownCommandMessage(command, { interactive: true }));
1087
+ return;
1934
1088
  }
1935
-
1936
- // ── Commands with unique behavior (not data-driven) ──────────────
1937
- switch (command) {
1938
-
1939
- case 'fn': {
1940
- if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>] [--class-name=<class>]'); return; }
1941
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all, className: iflags.className, line: iflags.line });
1942
- if (!ok) { console.log(error); return; }
1943
- console.log(output.formatFnResult(result));
1944
- if (note) console.log(note);
1945
- break;
1946
- }
1947
-
1948
- case 'class': {
1949
- if (!arg) { console.log('Usage: class <name> [--file=<pattern>]'); return; }
1950
- const { ok, result, error, note } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines, line: iflags.line });
1951
- if (!ok) { console.log(error); return; }
1952
- console.log(output.formatClassResult(result));
1953
- if (note) console.log(note);
1954
- break;
1955
- }
1956
-
1957
- case 'lines': {
1958
- if (!arg) { console.log('Usage: lines <range> --file=<file> (or: lines <file> <range>)'); return; }
1959
- const iLinesTarget = parseLinesTarget(arg, iflags.file);
1960
- const { ok, result, error } = execute(index, 'lines', { file: iLinesTarget.file, range: iLinesTarget.range });
1961
- if (!ok) { console.log(error); return; }
1962
- console.log(output.formatLines(result));
1963
- break;
1964
- }
1965
-
1966
- case 'expand': {
1967
- if (!arg) {
1968
- console.log('Usage: expand <number>');
1969
- return;
1970
- }
1971
- // Whole-token integers only (fix #248: parseInt truncation).
1972
- if (!/^\d+$/.test(String(arg).trim())) {
1973
- console.log(`Invalid item number: "${arg}"`);
1974
- return;
1975
- }
1976
- const expandNum = parseInt(arg, 10);
1977
- let match, itemCount, symbolName;
1978
- if (cache) {
1979
- const lookup = cache.lookup(index.root, expandNum);
1980
- match = lookup.match;
1981
- itemCount = lookup.itemCount;
1982
- symbolName = lookup.symbolName;
1983
- } else {
1984
- const cached = loadExpandableItems(index.root);
1985
- const items = cached?.items || [];
1986
- match = items.find(i => i.num === expandNum);
1987
- itemCount = items.length;
1988
- }
1989
- const { ok, result, error } = execute(index, 'expand', {
1990
- match, itemNum: expandNum, itemCount, symbolName, validateRoot: true
1991
- });
1992
- if (!ok) { console.log(error); return; }
1993
- console.log(result.text);
1994
- break;
1995
- }
1996
-
1997
- case 'context': {
1998
- const { ok, result, error, note } = execute(index, 'context', { name: arg, ...iflags });
1999
- if (!ok) { console.log(error); return; }
2000
- const { text, expandable } = output.formatContext(result, {
2001
- expandHint: 'Use "expand <N>" to see code for item N',
2002
- showConfidence: iflags.showConfidence !== false,
2003
- compact: !!iflags.compact,
2004
- });
2005
- console.log(text);
2006
- if (iflags.expand) {
2007
- printInlineExpand(result, index.root);
2008
- }
2009
- if (note) console.log(note);
2010
- if (cache) {
2011
- cache.save(index.root, arg, iflags.file, expandable);
2012
- } else {
2013
- saveExpandableItems(expandable, index.root);
2014
- }
2015
- break;
2016
- }
2017
-
2018
- case 'deadcode': {
2019
- const { ok, result, error, note } = execute(index, 'deadcode', iflags);
2020
- if (!ok) { console.log(error); return; }
2021
- console.log(output.formatDeadcode(result, {
2022
- top: iflags.top,
2023
- decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
2024
- exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
2025
- externalContractHint: !iflags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
2026
- }));
2027
- if (note) console.log(note);
2028
- break;
2029
- }
2030
-
2031
- case 'search': {
2032
- const { ok, result, error, structural, note } = execute(index, 'search', { term: arg, ...iflags });
2033
- if (!ok) { console.log(error); return; }
2034
- if (structural) {
2035
- console.log(output.formatStructuralSearch(result));
2036
- } else {
2037
- console.log(output.formatSearch(result, arg));
2038
- }
2039
- if (note) console.log(note);
2040
- break;
2041
- }
2042
-
2043
- default: {
2044
- // ── Data-driven dispatch for standard commands ────────────
2045
- const entry = INTERACTIVE_DISPATCH[command];
2046
- if (!entry) {
2047
- console.log(`Unknown command: ${command}. Type "help" for available commands.`);
2048
- return;
2049
- }
2050
- const params = buildInteractiveParams(entry.params, arg, iflags);
2051
- const { ok, result, error, note } = execute(index, command, params);
2052
- if (!ok) { console.log(error); return; }
2053
- console.log(entry.format(result, arg, iflags, index));
2054
- if (note) console.log(note);
2055
- }
1089
+ const publicParams = buildPublicParams(command, arg, iflags);
1090
+ const publicExecution = execute(index, command, publicParams);
1091
+ if (!publicExecution.ok) {
1092
+ console.log(formatSurfaceMessage(publicExecution.error, 'cli'));
1093
+ return;
2056
1094
  }
1095
+ console.log(formatCliText(
1096
+ command,
1097
+ publicExecution.result,
1098
+ publicParams,
1099
+ publicExecution,
1100
+ iflags,
1101
+ ));
2057
1102
  }
2058
1103
 
2059
1104
  // ============================================================================