ucn 3.8.13 → 3.8.14

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 (43) hide show
  1. package/.claude/skills/ucn/SKILL.md +3 -1
  2. package/.github/workflows/ci.yml +13 -1
  3. package/README.md +1 -0
  4. package/cli/index.js +165 -246
  5. package/core/analysis.js +1400 -0
  6. package/core/build-worker.js +194 -0
  7. package/core/cache.js +105 -7
  8. package/core/callers.js +194 -64
  9. package/core/deadcode.js +22 -66
  10. package/core/discovery.js +9 -54
  11. package/core/execute.js +139 -54
  12. package/core/graph.js +615 -0
  13. package/core/output/analysis-ext.js +271 -0
  14. package/core/output/analysis.js +491 -0
  15. package/core/output/extraction.js +188 -0
  16. package/core/output/find.js +355 -0
  17. package/core/output/graph.js +399 -0
  18. package/core/output/refactoring.js +293 -0
  19. package/core/output/reporting.js +331 -0
  20. package/core/output/search.js +307 -0
  21. package/core/output/shared.js +271 -0
  22. package/core/output/tracing.js +416 -0
  23. package/core/output.js +15 -3293
  24. package/core/parallel-build.js +165 -0
  25. package/core/project.js +299 -3633
  26. package/core/registry.js +59 -0
  27. package/core/reporting.js +258 -0
  28. package/core/search.js +890 -0
  29. package/core/stacktrace.js +1 -1
  30. package/core/tracing.js +631 -0
  31. package/core/verify.js +10 -13
  32. package/eslint.config.js +43 -0
  33. package/jsconfig.json +10 -0
  34. package/languages/go.js +21 -2
  35. package/languages/html.js +8 -0
  36. package/languages/index.js +102 -40
  37. package/languages/java.js +13 -0
  38. package/languages/javascript.js +17 -1
  39. package/languages/python.js +14 -0
  40. package/languages/rust.js +13 -0
  41. package/languages/utils.js +1 -1
  42. package/mcp/server.js +45 -28
  43. package/package.json +8 -3
package/cli/index.js CHANGED
@@ -15,7 +15,7 @@ const { ProjectIndex } = require('../core/project');
15
15
  const { expandGlob, findProjectRoot } = require('../core/discovery');
16
16
  const output = require('../core/output');
17
17
  // pickBestDefinition moved to execute.js — no longer needed here
18
- const { getCliCommandSet, resolveCommand } = require('../core/registry');
18
+ const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
19
19
  const { execute } = require('../core/execute');
20
20
  const { ExpandCache } = require('../core/expand-cache');
21
21
 
@@ -115,6 +115,12 @@ function parseFlags(tokens) {
115
115
  showConfidence: !tokens.includes('--no-confidence'),
116
116
  minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
117
117
  framework: getValueFlag('--framework'),
118
+ workers: (() => {
119
+ const v = getValueFlag('--workers');
120
+ if (v === null) return undefined;
121
+ const n = parseInt(v, 10);
122
+ return isNaN(n) ? undefined : n;
123
+ })(),
118
124
  };
119
125
  }
120
126
 
@@ -142,7 +148,7 @@ const knownFlags = new Set([
142
148
  '--max-lines', '--class-name', '--limit', '--max-files',
143
149
  '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
144
150
  '--show-confidence', '--no-confidence', '--min-confidence',
145
- '--framework'
151
+ '--framework', '--workers'
146
152
  ]);
147
153
 
148
154
  // Handle help flag
@@ -171,7 +177,8 @@ const VALUE_FLAGS = new Set([
171
177
  '--add-param', '--remove-param', '--rename-to', '--default',
172
178
  '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
173
179
  '--type', '--param', '--receiver', '--returns', '--decorator',
174
- '--limit', '--max-files', '--min-confidence', '--stack', '--framework'
180
+ '--limit', '--max-files', '--min-confidence', '--stack', '--framework',
181
+ '--workers'
175
182
  ]);
176
183
 
177
184
  // Remove flags from args, then add args after -- (which are all positional)
@@ -346,11 +353,9 @@ function runFileCommand(filePath, command, arg) {
346
353
  );
347
354
  break;
348
355
  case 'fn':
349
- if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
350
356
  printOutput(result, output.formatFnResultJson, output.formatFnResult);
351
357
  break;
352
358
  case 'class':
353
- if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
354
359
  printOutput(result, output.formatClassResultJson, output.formatClassResult);
355
360
  break;
356
361
  case 'lines':
@@ -420,7 +425,7 @@ function runProjectCommand(rootDir, command, arg) {
420
425
  // If cache was loaded but stale, force rebuild to avoid duplicates
421
426
  let needsCacheSave = false;
422
427
  if (!usedCache) {
423
- index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles });
428
+ index.build(null, { quiet: flags.quiet, forceRebuild: cacheWasLoaded, followSymlinks: flags.followSymlinks, maxFiles: flags.maxFiles, workers: flags.workers });
424
429
  needsCacheSave = flags.cache;
425
430
  }
426
431
 
@@ -428,6 +433,23 @@ function runProjectCommand(rootDir, command, arg) {
428
433
  // Resolve CLI aliases to canonical command names — dispatch on canonical
429
434
  const canonical = resolveCommand(command, 'cli') || command;
430
435
 
436
+ // Warn about flags that don't apply to this command
437
+ const applicableFlags = FLAG_APPLICABILITY[canonical];
438
+ if (applicableFlags) {
439
+ // Map from camelCase flag name to CLI flag string
440
+ const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
441
+ // Flags that are global (not command-specific) or have truthy defaults — skip warning for these
442
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', 'stack', 'showConfidence']);
443
+ for (const [key, value] of Object.entries(flags)) {
444
+ if (globalFlags.has(key)) continue;
445
+ // Skip falsy/default values (0, undefined, false, empty array)
446
+ if (!value || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
447
+ if (!applicableFlags.includes(key)) {
448
+ console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
449
+ }
450
+ }
451
+ }
452
+
431
453
  switch (canonical) {
432
454
  // ── Commands using shared executor ───────────────────────────────
433
455
 
@@ -475,7 +497,7 @@ function runProjectCommand(rootDir, command, arg) {
475
497
  }
476
498
 
477
499
  case 'context': {
478
- const { ok, result: ctx, error } = execute(index, 'context', { name: arg, ...flags });
500
+ const { ok, result: ctx, error, note } = execute(index, 'context', { name: arg, ...flags });
479
501
  if (!ok) fail(error);
480
502
  if (flags.json) {
481
503
  console.log(output.formatContextJson(ctx));
@@ -513,6 +535,7 @@ function runProjectCommand(rootDir, command, arg) {
513
535
 
514
536
  // Save expandable items to cache for 'expand' command
515
537
  saveExpandableItems(expandable, index.root);
538
+ if (note) console.error(note);
516
539
  }
517
540
  break;
518
541
  }
@@ -554,16 +577,18 @@ function runProjectCommand(rootDir, command, arg) {
554
577
  }
555
578
 
556
579
  case 'impact': {
557
- const { ok, result, error } = execute(index, 'impact', { name: arg, ...flags });
580
+ const { ok, result, error, note } = execute(index, 'impact', { name: arg, ...flags });
558
581
  if (!ok) fail(error);
559
582
  printOutput(result, output.formatImpactJson, output.formatImpact);
583
+ if (note) console.error(note);
560
584
  break;
561
585
  }
562
586
 
563
587
  case 'blast': {
564
- const { ok, result, error } = execute(index, 'blast', { name: arg, ...flags });
588
+ const { ok, result, error, note } = execute(index, 'blast', { name: arg, ...flags });
565
589
  if (!ok) fail(error);
566
590
  printOutput(result, output.formatBlastJson, output.formatBlast);
591
+ if (note) console.error(note);
567
592
  break;
568
593
  }
569
594
 
@@ -575,16 +600,18 @@ function runProjectCommand(rootDir, command, arg) {
575
600
  }
576
601
 
577
602
  case 'trace': {
578
- const { ok, result, error } = execute(index, 'trace', { name: arg, ...flags });
603
+ const { ok, result, error, note } = execute(index, 'trace', { name: arg, ...flags });
579
604
  if (!ok) fail(error);
580
605
  printOutput(result, output.formatTraceJson, output.formatTrace);
606
+ if (note) console.error(note);
581
607
  break;
582
608
  }
583
609
 
584
610
  case 'reverseTrace': {
585
- const { ok, result, error } = execute(index, 'reverseTrace', { name: arg, ...flags });
611
+ const { ok, result, error, note } = execute(index, 'reverseTrace', { name: arg, ...flags });
586
612
  if (!ok) fail(error);
587
613
  printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
614
+ if (note) console.error(note);
588
615
  break;
589
616
  }
590
617
 
@@ -603,9 +630,10 @@ function runProjectCommand(rootDir, command, arg) {
603
630
  }
604
631
 
605
632
  case 'related': {
606
- const { ok, result, error } = execute(index, 'related', { name: arg, ...flags });
633
+ const { ok, result, error, note } = execute(index, 'related', { name: arg, ...flags });
607
634
  if (!ok) fail(error);
608
635
  printOutput(result, output.formatRelatedJson, r => output.formatRelated(r, { all: flags.all, top: flags.top }));
636
+ if (note) console.error(note);
609
637
  break;
610
638
  }
611
639
 
@@ -613,18 +641,18 @@ function runProjectCommand(rootDir, command, arg) {
613
641
 
614
642
  case 'fn': {
615
643
  requireArg(arg, 'Usage: ucn . fn <name>');
616
- const { ok, result, error } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all });
644
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all });
617
645
  if (!ok) fail(error);
618
- if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
646
+ if (note) console.error(note);
619
647
  printOutput(result, output.formatFnResultJson, output.formatFnResult);
620
648
  break;
621
649
  }
622
650
 
623
651
  case 'class': {
624
652
  requireArg(arg, 'Usage: ucn . class <name>');
625
- const { ok, result, error } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
653
+ const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
626
654
  if (!ok) fail(error);
627
- if (result.notes.length) result.notes.forEach(n => console.error('Note: ' + n));
655
+ if (note) console.error(note);
628
656
  printOutput(result, output.formatClassResultJson, output.formatClassResult);
629
657
  break;
630
658
  }
@@ -640,37 +668,41 @@ function runProjectCommand(rootDir, command, arg) {
640
668
  // ── File dependency commands ────────────────────────────────────
641
669
 
642
670
  case 'imports': {
643
- const { ok, result, error } = execute(index, 'imports', { file: arg });
671
+ const filePath = arg || flags.file;
672
+ const { ok, result, error } = execute(index, 'imports', { file: filePath });
644
673
  if (!ok) fail(error);
645
674
  printOutput(result,
646
- r => output.formatImportsJson(r, arg),
647
- r => output.formatImports(r, arg)
675
+ r => output.formatImportsJson(r, filePath),
676
+ r => output.formatImports(r, filePath)
648
677
  );
649
678
  break;
650
679
  }
651
680
 
652
681
  case 'exporters': {
653
- const { ok, result, error } = execute(index, 'exporters', { file: arg });
682
+ const filePath = arg || flags.file;
683
+ const { ok, result, error } = execute(index, 'exporters', { file: filePath });
654
684
  if (!ok) fail(error);
655
685
  printOutput(result,
656
- r => output.formatExportersJson(r, arg),
657
- r => output.formatExporters(r, arg)
686
+ r => output.formatExportersJson(r, filePath),
687
+ r => output.formatExporters(r, filePath)
658
688
  );
659
689
  break;
660
690
  }
661
691
 
662
692
  case 'fileExports': {
663
- const { ok, result, error } = execute(index, 'fileExports', { file: arg });
693
+ const filePath = arg || flags.file;
694
+ const { ok, result, error } = execute(index, 'fileExports', { file: filePath });
664
695
  if (!ok) fail(error);
665
696
  printOutput(result,
666
- r => JSON.stringify({ file: arg, exports: r }, null, 2),
667
- r => output.formatFileExports(r, arg)
697
+ r => JSON.stringify({ file: filePath, exports: r }, null, 2),
698
+ r => output.formatFileExports(r, filePath)
668
699
  );
669
700
  break;
670
701
  }
671
702
 
672
703
  case 'graph': {
673
- const { ok, result, error } = execute(index, 'graph', { file: arg, direction: flags.direction, depth: flags.depth, all: flags.all });
704
+ const filePath = arg || flags.file;
705
+ const { ok, result, error } = execute(index, 'graph', { file: filePath, direction: flags.direction, depth: flags.depth, all: flags.all });
674
706
  if (!ok) fail(error);
675
707
  printOutput(result,
676
708
  r => JSON.stringify({
@@ -678,7 +710,7 @@ function runProjectCommand(rootDir, command, arg) {
678
710
  nodes: r.nodes.map(n => ({ file: n.relativePath, depth: n.depth })),
679
711
  edges: r.edges.map(e => ({ from: path.relative(index.root, e.from), to: path.relative(index.root, e.to) }))
680
712
  }, null, 2),
681
- r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: arg })
713
+ r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth != null ? parseInt(flags.depth, 10) : 2, file: filePath })
682
714
  );
683
715
  break;
684
716
  }
@@ -713,19 +745,21 @@ function runProjectCommand(rootDir, command, arg) {
713
745
  }
714
746
 
715
747
  case 'affectedTests': {
716
- const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...flags });
748
+ const { ok, result, error, note } = execute(index, 'affectedTests', { name: arg, ...flags });
717
749
  if (!ok) fail(error);
718
750
  printOutput(result, output.formatAffectedTestsJson, r => output.formatAffectedTests(r, { all: flags.all }));
751
+ if (note) console.error(note);
719
752
  break;
720
753
  }
721
754
 
722
755
  case 'api': {
723
- const { ok, result, error, note } = execute(index, 'api', { file: arg || flags.file, limit: flags.limit });
756
+ const filePath = arg || flags.file;
757
+ const { ok, result, error, note } = execute(index, 'api', { file: filePath, limit: flags.limit });
724
758
  if (!ok) fail(error);
725
759
  if (note) console.error(note);
726
760
  printOutput(result,
727
- r => output.formatApiJson(r, arg),
728
- r => output.formatApi(r, arg)
761
+ r => output.formatApiJson(r, filePath),
762
+ r => output.formatApi(r, filePath)
729
763
  );
730
764
  break;
731
765
  }
@@ -864,7 +898,7 @@ function runGlobCommand(pattern, command, arg) {
864
898
  }
865
899
 
866
900
  switch (command) {
867
- case 'toc':
901
+ case 'toc': {
868
902
  let totalFunctions = 0;
869
903
  let totalClasses = 0;
870
904
  let totalState = 0;
@@ -915,6 +949,7 @@ function runGlobCommand(pattern, command, arg) {
915
949
  }));
916
950
  }
917
951
  break;
952
+ }
918
953
 
919
954
  case 'find':
920
955
  if (!arg) {
@@ -1113,33 +1148,37 @@ Common Flags:
1113
1148
  --file <pattern> Filter by file path (e.g., --file=routes)
1114
1149
  --exclude=a,b Exclude patterns (e.g., --exclude=test,mock)
1115
1150
  --in=<path> Only in path (e.g., --in=src/core)
1116
- --depth=N Trace/graph depth (default: 3, also expands all children)
1151
+ --depth=N Max depth: blast=3, trace=3, reverse-trace=5, graph=2, affected-tests=3
1117
1152
  --direction=X Graph direction: imports, importers, or both (default: both)
1118
- --all Expand truncated sections (about, trace, graph, related)
1119
- --top=N Limit results (find, deadcode)
1120
- --limit=N Limit result count (find, usages, search, deadcode, api, toc)
1153
+ --all Show full results: all callers/callees (about), full tree (trace/blast),
1154
+ all names (related/find/fn/class/toc), all changed (diff-impact)
1155
+ --top=N Limit callers/callees (about), similar functions (related), search results
1156
+ --limit=N Limit result count (find, usages, search, deadcode, api, toc, entrypoints, diff-impact)
1121
1157
  --max-files=N Max files to index (large projects)
1122
- --context=N Lines of context around matches
1158
+ --context=N Lines of context around matches (search, usages)
1123
1159
  --json Machine-readable output
1124
- --code-only Filter out comments and strings
1125
- --with-types Include type definitions
1160
+ --code-only Filter out comments/strings (search, usages)
1161
+ --with-types Include type definitions (about, smart)
1162
+ --detailed Show all symbols in toc (not just counts)
1126
1163
  --include-tests Include test files
1127
1164
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1128
1165
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1129
1166
  --include-uncertain Include ambiguous/uncertain matches
1130
- --no-confidence Hide confidence scores (shown by default)
1131
- --min-confidence=N Filter edges below confidence threshold (0.0-1.0)
1167
+ --no-confidence Hide confidence scores (shown by default in about, context)
1168
+ --min-confidence=N Filter low-confidence edges (about, context, blast, trace,
1169
+ reverse-trace, smart, affected-tests)
1170
+ --show-confidence Show confidence scores on caller/callee edges (about, context)
1132
1171
  --include-exported Include exported symbols in deadcode
1133
1172
  --no-regex Force plain text search (regex is default)
1134
1173
  --functions Show per-function line counts (stats command)
1135
1174
  --include-decorated Include decorated/annotated symbols in deadcode
1136
1175
  --framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
1137
- --exact Exact name match only (find)
1176
+ --exact Exact name match only (find, typedef)
1138
1177
  --calls-only Only show call/test-case matches (tests)
1139
1178
  --case-sensitive Case-sensitive text search (search)
1140
- --detailed List all symbols in toc (compact by default)
1141
1179
  --top-level Show only top-level functions in toc
1142
1180
  --max-lines=N Max source lines for class (large classes show summary)
1181
+ --workers=N Parallel build workers (auto-detect; 0 to disable, env: UCN_WORKERS)
1143
1182
  --no-cache Disable caching
1144
1183
  --clear-cache Clear cache before running
1145
1184
  --base=<ref> Git ref for diff-impact (default: HEAD)
@@ -1165,7 +1204,7 @@ function runInteractive(rootDir) {
1165
1204
 
1166
1205
  console.log('Building index...');
1167
1206
  const index = new ProjectIndex(rootDir);
1168
- index.build(null, { quiet: true });
1207
+ index.build(null, { quiet: true, workers: flags.workers });
1169
1208
  const iExpandCache = new ExpandCache({ maxSize: 20 });
1170
1209
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
1171
1210
  console.log('Type commands (e.g., "find parseFile", "about main", "toc")');
@@ -1239,7 +1278,7 @@ Flags can be added per-command: context myFunc --include-methods
1239
1278
 
1240
1279
  if (input === 'rebuild') {
1241
1280
  console.log('Rebuilding index...');
1242
- index.build(null, { quiet: true, forceRebuild: true });
1281
+ index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
1243
1282
  console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
1244
1283
  rl.prompt();
1245
1284
  return;
@@ -1286,25 +1325,89 @@ Flags can be added per-command: context myFunc --include-methods
1286
1325
 
1287
1326
  // parseInteractiveFlags removed — both global and interactive mode now use parseFlags()
1288
1327
 
1328
+ // ── Data-driven interactive command dispatch ─────────────────────────────
1329
+ //
1330
+ // Each entry maps a canonical command name to:
1331
+ // params: (arg, iflags) => execute() params object
1332
+ // format: (result, arg, iflags, index) => formatted string
1333
+ //
1334
+ // The generic handler calls execute(), checks errors, prints notes, and
1335
+ // formats the result. Only commands with truly unique behavior (expand
1336
+ // cache save, file writing, conditional formatters) keep explicit cases.
1337
+
1338
+ const INTERACTIVE_DISPATCH = {
1339
+ // ── Understanding Code ───────────────────────────────────────────
1340
+ 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 }) },
1341
+ smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
1342
+ impact: { params: 'name', format: (r) => output.formatImpact(r) },
1343
+ blast: { params: 'name', format: (r) => output.formatBlast(r) },
1344
+ trace: { params: 'name', format: (r) => output.formatTrace(r) },
1345
+ reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
1346
+ related: { params: 'name', format: (r, _a, f) => output.formatRelated(r, { all: f.all, top: f.top }) },
1347
+ example: { params: (a) => ({ name: a }), format: (r, a) => output.formatExample(r, a) },
1348
+
1349
+ // ── Finding Code ─────────────────────────────────────────────────
1350
+ find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
1351
+ usages: { params: 'name', format: (r, a) => output.formatUsages(r, a) },
1352
+ 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: 'use --include-uncertain to include all' }) },
1353
+ tests: { params: 'name', format: (r, a) => output.formatTests(r, a) },
1354
+ affectedTests: { params: 'name', format: (r, _a, f) => output.formatAffectedTests(r, { all: f.all }) },
1355
+ typedef: { params: 'name', format: (r, a) => output.formatTypedef(r, a) },
1356
+
1357
+ // ── File Dependencies ────────────────────────────────────────────
1358
+ imports: { params: 'file', format: (r, a, f) => output.formatImports(r, a || f.file) },
1359
+ exporters: { params: 'file', format: (r, a, f) => output.formatExporters(r, a || f.file) },
1360
+ fileExports: { params: 'file', format: (r, a, f) => output.formatFileExports(r, a || f.file) },
1361
+ 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 }); } },
1362
+ circularDeps: { params: (a, f) => ({ file: f.file, exclude: f.exclude }), format: (r) => output.formatCircularDeps(r) },
1363
+
1364
+ // ── Refactoring Helpers ──────────────────────────────────────────
1365
+ plan: { params: 'name', format: (r) => output.formatPlan(r) },
1366
+ verify: { params: 'name', format: (r) => output.formatVerify(r) },
1367
+ diffImpact: { params: 'flags', format: (r, _a, f) => output.formatDiffImpact(r, { all: f.all }) },
1368
+ entrypoints: { params: (a, f) => ({ type: f.type, framework: f.framework, file: f.file, exclude: f.exclude }), format: (r) => output.formatEntrypoints(r) },
1369
+
1370
+ // ── Other ────────────────────────────────────────────────────────
1371
+ api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file || '.') },
1372
+ stacktrace: { params: (a) => ({ stack: a }), format: (r) => output.formatStackTrace(r) },
1373
+ stats: { params: 'flags', format: (r, _a, f) => output.formatStats(r, { top: f.top }) },
1374
+ };
1375
+
1376
+ /**
1377
+ * Build execute() params from a dispatch entry's params descriptor.
1378
+ * 'name' → { name: arg, ...iflags }
1379
+ * 'file' → { file: arg }
1380
+ * 'flags' → iflags (no arg)
1381
+ * function → custom builder
1382
+ */
1383
+ function buildInteractiveParams(descriptor, arg, iflags) {
1384
+ if (typeof descriptor === 'function') return descriptor(arg, iflags);
1385
+ switch (descriptor) {
1386
+ case 'name': return { name: arg, ...iflags };
1387
+ case 'file': return { file: arg || iflags.file };
1388
+ case 'flags': return iflags;
1389
+ default: return { name: arg, ...iflags };
1390
+ }
1391
+ }
1392
+
1289
1393
  function executeInteractiveCommand(index, command, arg, iflags = {}, cache = null) {
1394
+ // ── Commands with unique behavior (not data-driven) ──────────────
1290
1395
  switch (command) {
1291
1396
 
1292
- // ── Extraction commands (via execute) ────────────────────────────
1293
-
1294
1397
  case 'fn': {
1295
1398
  if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>]'); return; }
1296
- const { ok, result, error } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all });
1399
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all });
1297
1400
  if (!ok) { console.log(error); return; }
1298
- if (result.notes.length) result.notes.forEach(n => console.log('Note: ' + n));
1401
+ if (note) console.log(note);
1299
1402
  console.log(output.formatFnResult(result));
1300
1403
  break;
1301
1404
  }
1302
1405
 
1303
1406
  case 'class': {
1304
1407
  if (!arg) { console.log('Usage: class <name> [--file=<pattern>]'); return; }
1305
- const { ok, result, error } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines });
1408
+ const { ok, result, error, note } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines });
1306
1409
  if (!ok) { console.log(error); return; }
1307
- if (result.notes.length) result.notes.forEach(n => console.log('Note: ' + n));
1410
+ if (note) console.log(note);
1308
1411
  console.log(output.formatClassResult(result));
1309
1412
  break;
1310
1413
  }
@@ -1347,16 +1450,6 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1347
1450
  break;
1348
1451
  }
1349
1452
 
1350
- case 'find': {
1351
- const { ok, result, error, note } = execute(index, 'find', { name: arg, ...iflags });
1352
- if (!ok) { console.log(error); return; }
1353
- if (note) console.log(note);
1354
- console.log(output.formatFindDetailed(result, arg, { depth: iflags.depth, top: iflags.top, all: iflags.all }));
1355
- break;
1356
- }
1357
-
1358
- // ── context: needs expandable items cache ────────────────────────
1359
-
1360
1453
  case 'context': {
1361
1454
  const { ok, result, error } = execute(index, 'context', { name: arg, ...iflags });
1362
1455
  if (!ok) { console.log(error); return; }
@@ -1375,8 +1468,6 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1375
1468
  break;
1376
1469
  }
1377
1470
 
1378
- // ── deadcode: needs result fields for hint construction ──────────
1379
-
1380
1471
  case 'deadcode': {
1381
1472
  const { ok, result, error } = execute(index, 'deadcode', iflags);
1382
1473
  if (!ok) { console.log(error); return; }
@@ -1388,126 +1479,6 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1388
1479
  break;
1389
1480
  }
1390
1481
 
1391
- case 'entrypoints': {
1392
- const { ok, result, error } = execute(index, 'entrypoints', { type: iflags.type, framework: iflags.framework, file: iflags.file, exclude: iflags.exclude });
1393
- if (!ok) { console.log(error); return; }
1394
- console.log(output.formatEntrypoints(result));
1395
- break;
1396
- }
1397
-
1398
- // ── Standard commands routed through execute() ───────────────────
1399
-
1400
- case 'toc': {
1401
- const { ok, result, error } = execute(index, 'toc', iflags);
1402
- if (!ok) { console.log(error); return; }
1403
- console.log(output.formatToc(result, {
1404
- detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol',
1405
- uncertainHint: 'use --include-uncertain to include all'
1406
- }));
1407
- break;
1408
- }
1409
-
1410
- case 'about': {
1411
- const { ok, result, error } = execute(index, 'about', { name: arg, ...iflags });
1412
- if (!ok) { console.log(error); return; }
1413
- console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth, showConfidence: iflags.showConfidence }));
1414
- break;
1415
- }
1416
-
1417
- case 'usages': {
1418
- const { ok, result, error } = execute(index, 'usages', { name: arg, ...iflags });
1419
- if (!ok) { console.log(error); return; }
1420
- console.log(output.formatUsages(result, arg));
1421
- break;
1422
- }
1423
-
1424
- case 'smart': {
1425
- const { ok, result, error } = execute(index, 'smart', { name: arg, ...iflags });
1426
- if (!ok) { console.log(error); return; }
1427
- console.log(output.formatSmart(result, {
1428
- uncertainHint: 'use --include-uncertain to include all'
1429
- }));
1430
- break;
1431
- }
1432
-
1433
- case 'impact': {
1434
- const { ok, result, error } = execute(index, 'impact', { name: arg, ...iflags });
1435
- if (!ok) { console.log(error); return; }
1436
- console.log(output.formatImpact(result));
1437
- break;
1438
- }
1439
-
1440
- case 'blast': {
1441
- const { ok, result, error } = execute(index, 'blast', { name: arg, ...iflags });
1442
- if (!ok) { console.log(error); return; }
1443
- console.log(output.formatBlast(result));
1444
- break;
1445
- }
1446
-
1447
- case 'trace': {
1448
- const { ok, result, error } = execute(index, 'trace', { name: arg, ...iflags });
1449
- if (!ok) { console.log(error); return; }
1450
- console.log(output.formatTrace(result));
1451
- break;
1452
- }
1453
-
1454
- case 'reverseTrace': {
1455
- const { ok, result, error } = execute(index, 'reverseTrace', { name: arg, ...iflags });
1456
- if (!ok) { console.log(error); return; }
1457
- console.log(output.formatReverseTrace(result));
1458
- break;
1459
- }
1460
-
1461
- case 'graph': {
1462
- const { ok, result, error } = execute(index, 'graph', { file: arg || iflags.file, direction: iflags.direction, depth: iflags.depth, all: iflags.all });
1463
- if (!ok) { console.log(error); return; }
1464
- const graphDepth = iflags.depth ? parseInt(iflags.depth) : 2;
1465
- console.log(output.formatGraph(result, { showAll: iflags.all || !!iflags.depth, maxDepth: graphDepth, file: arg }));
1466
- break;
1467
- }
1468
-
1469
- case 'circularDeps': {
1470
- const { ok, result, error } = execute(index, 'circularDeps', { file: iflags.file, exclude: iflags.exclude });
1471
- if (!ok) { console.log(error); return; }
1472
- console.log(output.formatCircularDeps(result));
1473
- break;
1474
- }
1475
-
1476
- case 'fileExports': {
1477
- const { ok, result, error } = execute(index, 'fileExports', { file: arg });
1478
- if (!ok) { console.log(error); return; }
1479
- console.log(output.formatFileExports(result, arg));
1480
- break;
1481
- }
1482
-
1483
- case 'imports': {
1484
- const { ok, result, error } = execute(index, 'imports', { file: arg });
1485
- if (!ok) { console.log(error); return; }
1486
- console.log(output.formatImports(result, arg));
1487
- break;
1488
- }
1489
-
1490
- case 'exporters': {
1491
- const { ok, result, error } = execute(index, 'exporters', { file: arg });
1492
- if (!ok) { console.log(error); return; }
1493
- console.log(output.formatExporters(result, arg));
1494
- break;
1495
- }
1496
-
1497
- case 'tests': {
1498
- const { ok, result, error } = execute(index, 'tests', { name: arg, ...iflags });
1499
- if (!ok) { console.log(error); return; }
1500
- console.log(output.formatTests(result, arg));
1501
- break;
1502
- }
1503
-
1504
- case 'affectedTests': {
1505
- const { ok, result, error } = execute(index, 'affectedTests', { name: arg, ...iflags });
1506
- if (!ok) { console.log(error); return; }
1507
- console.log(output.formatAffectedTests(result, { all: iflags.all }));
1508
- break;
1509
- }
1510
-
1511
1482
  case 'search': {
1512
1483
  const { ok, result, error, structural } = execute(index, 'search', { term: arg, ...iflags });
1513
1484
  if (!ok) { console.log(error); return; }
@@ -1519,71 +1490,19 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1519
1490
  break;
1520
1491
  }
1521
1492
 
1522
- case 'typedef': {
1523
- const { ok, result, error } = execute(index, 'typedef', { name: arg, ...iflags });
1524
- if (!ok) { console.log(error); return; }
1525
- console.log(output.formatTypedef(result, arg));
1526
- break;
1527
- }
1528
-
1529
- case 'api': {
1530
- const { ok, result, error } = execute(index, 'api', { file: arg });
1531
- if (!ok) { console.log(error); return; }
1532
- console.log(output.formatApi(result, arg || '.'));
1533
- break;
1534
- }
1535
-
1536
- case 'diffImpact': {
1537
- const { ok, result, error } = execute(index, 'diffImpact', iflags);
1538
- if (!ok) { console.log(error); return; }
1539
- console.log(output.formatDiffImpact(result, { all: iflags.all }));
1540
- break;
1541
- }
1542
-
1543
- case 'stats': {
1544
- const { ok, result, error } = execute(index, 'stats', iflags);
1545
- if (!ok) { console.log(error); return; }
1546
- console.log(output.formatStats(result, { top: iflags.top }));
1547
- break;
1548
- }
1549
-
1550
- case 'related': {
1551
- const { ok, result, error } = execute(index, 'related', { name: arg, ...iflags });
1552
- if (!ok) { console.log(error); return; }
1553
- console.log(output.formatRelated(result, { all: iflags.all, top: iflags.top }));
1554
- break;
1555
- }
1556
-
1557
- case 'example': {
1558
- const { ok, result, error } = execute(index, 'example', { name: arg });
1559
- if (!ok) { console.log(error); return; }
1560
- console.log(output.formatExample(result, arg));
1561
- break;
1562
- }
1563
-
1564
- case 'plan': {
1565
- const { ok, result, error } = execute(index, 'plan', { name: arg, ...iflags });
1566
- if (!ok) { console.log(error); return; }
1567
- console.log(output.formatPlan(result));
1568
- break;
1569
- }
1570
-
1571
- case 'verify': {
1572
- const { ok, result, error } = execute(index, 'verify', { name: arg, ...iflags });
1573
- if (!ok) { console.log(error); return; }
1574
- console.log(output.formatVerify(result));
1575
- break;
1576
- }
1577
-
1578
- case 'stacktrace': {
1579
- const { ok, result, error } = execute(index, 'stacktrace', { stack: arg });
1493
+ default: {
1494
+ // ── Data-driven dispatch for standard commands ────────────
1495
+ const entry = INTERACTIVE_DISPATCH[command];
1496
+ if (!entry) {
1497
+ console.log(`Unknown command: ${command}. Type "help" for available commands.`);
1498
+ return;
1499
+ }
1500
+ const params = buildInteractiveParams(entry.params, arg, iflags);
1501
+ const { ok, result, error, note } = execute(index, command, params);
1580
1502
  if (!ok) { console.log(error); return; }
1581
- console.log(output.formatStackTrace(result));
1582
- break;
1503
+ if (note) console.log(note);
1504
+ console.log(entry.format(result, arg, iflags, index));
1583
1505
  }
1584
-
1585
- default:
1586
- console.log(`Unknown command: ${command}. Type "help" for available commands.`);
1587
1506
  }
1588
1507
  }
1589
1508