ucn 3.8.18 → 3.8.20

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.
@@ -133,7 +133,7 @@ ucn entrypoints --file=routes/ # Scoped to files
133
133
  | File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Setting `--depth=N` expands all children. Can be noisy — use depth=1 for large projects. For function-level flow, use `trace` instead |
134
134
  | Are there circular dependencies? | `ucn circular-deps` | Detect circular import chains. `--file=<pattern>` filters to cycles involving a file. `--exclude=test` skips test files |
135
135
  | What are the framework entry points? | `ucn entrypoints` | Lists all detected routes, DI beans, tasks, etc. Filter: `--type=http`, `--framework=express` |
136
- | Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
136
+ | Find which tests cover a function | `ucn tests <name>` | Test files and test function names. Scope with `--file`, `--class-name`, `--exclude`, `--calls-only` |
137
137
  | Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
138
138
  | Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
139
139
  | See a project's public API | `ucn api` or `ucn api --file=<file>` | All exported/public symbols with signatures |
@@ -163,7 +163,7 @@ ucn [target] <command> [name] [--flags]
163
163
  | `--in=src/core` | Limit search to a subdirectory |
164
164
  | `--depth=N` | Control tree depth for `trace`, `graph`, and detail level for `find` (default 3). Also expands all children — no breadth limit |
165
165
  | `--all` | Expand truncated sections in `about`, `trace`, `graph`, `related` |
166
- | `--include-tests` | Include test files in results (excluded by default) |
166
+ | `--include-tests` | Include test files in usage counts (`about`) and results (`find`, `usages`, `deadcode`). Callers always include tests. |
167
167
  | `--include-methods` | Include `obj.method()` calls in `context`/`smart` (only direct calls shown by default) |
168
168
  | `--base=<ref>` | Git ref for diff-impact (default: HEAD) |
169
169
  | `--staged` | Analyze staged changes (diff-impact) |
package/cli/index.js CHANGED
@@ -74,19 +74,19 @@ function parseFlags(tokens) {
74
74
  exclude: parseExclude(),
75
75
  in: getValueFlag('--in'),
76
76
  includeTests: tokens.includes('--include-tests') ? true : undefined,
77
- includeExported: tokens.includes('--include-exported'),
78
- includeDecorated: tokens.includes('--include-decorated'),
79
- includeUncertain: tokens.includes('--include-uncertain'),
77
+ includeExported: tokens.includes('--include-exported') || undefined,
78
+ includeDecorated: tokens.includes('--include-decorated') || undefined,
79
+ includeUncertain: tokens.includes('--include-uncertain') || undefined,
80
80
  includeMethods: tokens.some(a => a === '--include-methods=false') ? false : tokens.some(a => a === '--include-methods' || (a.startsWith('--include-methods=') && a !== '--include-methods=false')) ? true : undefined,
81
- detailed: tokens.includes('--detailed'),
82
- topLevel: tokens.includes('--top-level'),
83
- all: tokens.includes('--all'),
84
- exact: tokens.includes('--exact'),
85
- callsOnly: tokens.includes('--calls-only'),
86
- codeOnly: tokens.includes('--code-only'),
87
- caseSensitive: tokens.includes('--case-sensitive'),
88
- withTypes: tokens.includes('--with-types'),
89
- expand: tokens.includes('--expand'),
81
+ detailed: tokens.includes('--detailed') || undefined,
82
+ topLevel: tokens.includes('--top-level') || undefined,
83
+ all: tokens.includes('--all') || undefined,
84
+ exact: tokens.includes('--exact') || undefined,
85
+ callsOnly: tokens.includes('--calls-only') || undefined,
86
+ codeOnly: tokens.includes('--code-only') || undefined,
87
+ caseSensitive: tokens.includes('--case-sensitive') || undefined,
88
+ withTypes: tokens.includes('--with-types') || undefined,
89
+ expand: tokens.includes('--expand') || undefined,
90
90
  depth: getValueFlag('--depth'),
91
91
  top: parseInt(getValueFlag('--top') || '0'),
92
92
  context: parseInt(getValueFlag('--context') || '0'),
@@ -96,10 +96,10 @@ function parseFlags(tokens) {
96
96
  renameTo: getValueFlag('--rename-to'),
97
97
  defaultValue: getValueFlag('--default'),
98
98
  base: getValueFlag('--base'),
99
- staged: tokens.includes('--staged'),
99
+ staged: tokens.includes('--staged') || undefined,
100
100
  maxLines: getValueFlag('--max-lines') || null,
101
101
  regex: tokens.includes('--no-regex') ? false : undefined,
102
- functions: tokens.includes('--functions'),
102
+ functions: tokens.includes('--functions') || undefined,
103
103
  className: getValueFlag('--class-name'),
104
104
  limit: parseInt(getValueFlag('--limit') || '0') || undefined,
105
105
  maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
@@ -109,9 +109,9 @@ function parseFlags(tokens) {
109
109
  receiver: getValueFlag('--receiver'),
110
110
  returns: getValueFlag('--returns'),
111
111
  decorator: getValueFlag('--decorator'),
112
- exported: tokens.includes('--exported'),
113
- unused: tokens.includes('--unused'),
114
- showConfidence: !tokens.includes('--no-confidence'),
112
+ exported: tokens.includes('--exported') || undefined,
113
+ unused: tokens.includes('--unused') || undefined,
114
+ showConfidence: tokens.includes('--no-confidence') ? false : undefined,
115
115
  minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
116
116
  framework: getValueFlag('--framework'),
117
117
  stack: getValueFlag('--stack'),
@@ -227,6 +227,33 @@ function printOutput(result, jsonFn, textFn) {
227
227
  }
228
228
  }
229
229
 
230
+ /**
231
+ * Print inline 3-line code previews for each callee (--expand support).
232
+ * Used by context in project, interactive, and glob modes.
233
+ */
234
+ function printInlineExpand(ctx, root) {
235
+ if (!root || !ctx || !ctx.callees) return;
236
+ for (const c of ctx.callees) {
237
+ if (c.relativePath && c.startLine) {
238
+ try {
239
+ const filePath = path.join(root, c.relativePath);
240
+ const content = fs.readFileSync(filePath, 'utf-8');
241
+ const codeLines = content.split('\n');
242
+ const endLine = c.endLine || c.startLine + 5;
243
+ const previewLines = Math.min(3, endLine - c.startLine + 1);
244
+ for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
245
+ console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
246
+ }
247
+ if (endLine - c.startLine + 1 > 3) {
248
+ console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
249
+ }
250
+ } catch (e) {
251
+ // Skip on error
252
+ }
253
+ }
254
+ }
255
+ }
256
+
230
257
  // ============================================================================
231
258
  // MAIN
232
259
  // ============================================================================
@@ -304,6 +331,12 @@ function runFileCommand(filePath, command, arg) {
304
331
  if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
305
332
  effectiveArg = filePath;
306
333
  }
334
+ // Scope to the target file unless an explicit --file was provided
335
+ if (!flags.file) {
336
+ const relPath = path.relative(projectRoot, path.resolve(filePath));
337
+ flags.file = relPath;
338
+ flags._fileFromFileMode = true; // suppress inapplicable-flag warning
339
+ }
307
340
  runProjectCommand(projectRoot, command, effectiveArg);
308
341
  return;
309
342
  }
@@ -374,9 +407,11 @@ function runFileCommand(filePath, command, arg) {
374
407
  case 'typedef':
375
408
  printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
376
409
  break;
377
- case 'api':
378
- printOutput(result, r => output.formatApiJson(r, arg), r => output.formatApi(r, arg));
410
+ case 'api': {
411
+ const apiFile = relativePath;
412
+ printOutput(result, r => output.formatApiJson(r, apiFile), r => output.formatApi(r, apiFile));
379
413
  break;
414
+ }
380
415
  }
381
416
  }
382
417
 
@@ -443,12 +478,14 @@ function runProjectCommand(rootDir, command, arg) {
443
478
  if (applicableFlags) {
444
479
  // Map from camelCase flag name to CLI flag string
445
480
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
446
- // Flags that are global (not command-specific) or have truthy defaults — skip warning for these
447
- const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', 'showConfidence']);
481
+ // Flags that are global (not command-specific) — skip warning for these
482
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
448
483
  for (const [key, value] of Object.entries(flags)) {
449
484
  if (globalFlags.has(key)) continue;
450
- // Skip falsy/default values (0, undefined, false, empty array)
451
- if (!value || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
485
+ // Skip unset values (undefined, null, 0, empty array) — but NOT false (explicit negation)
486
+ if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
487
+ // Skip --file when it was injected by file-mode routing, not user input
488
+ if (key === 'file' && flags._fileFromFileMode) continue;
452
489
  if (!applicableFlags.includes(key)) {
453
490
  console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
454
491
  }
@@ -509,33 +546,15 @@ function runProjectCommand(rootDir, command, arg) {
509
546
  } else {
510
547
  const { text, expandable } = output.formatContext(ctx, {
511
548
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
512
- expandHint: 'Use "ucn . expand <N>" to see code for item N',
549
+ expandHint: 'Use "expand <N>" or --expand to see code for items',
513
550
  uncertainHint: 'use --include-uncertain to include all',
514
- showConfidence: flags.showConfidence,
551
+ showConfidence: flags.showConfidence !== false,
515
552
  });
516
553
  console.log(text);
517
554
 
518
555
  // Inline expansion of callees when --expand flag is set
519
- if (flags.expand && index.root && ctx.callees) {
520
- for (const c of ctx.callees) {
521
- if (c.relativePath && c.startLine) {
522
- try {
523
- const filePath = path.join(index.root, c.relativePath);
524
- const content = fs.readFileSync(filePath, 'utf-8');
525
- const codeLines = content.split('\n');
526
- const endLine = c.endLine || c.startLine + 5;
527
- const previewLines = Math.min(3, endLine - c.startLine + 1);
528
- for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
529
- console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
530
- }
531
- if (endLine - c.startLine + 1 > 3) {
532
- console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
533
- }
534
- } catch (e) {
535
- // Skip on error
536
- }
537
- }
538
- }
556
+ if (flags.expand) {
557
+ printInlineExpand(ctx, index.root);
539
558
  }
540
559
 
541
560
  // Save expandable items to cache for 'expand' command
@@ -577,7 +596,7 @@ function runProjectCommand(rootDir, command, arg) {
577
596
  if (!ok) fail(error);
578
597
  printOutput(result,
579
598
  output.formatAboutJson,
580
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence })
599
+ r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false })
581
600
  );
582
601
  if (note) console.error(note);
583
602
  break;
@@ -739,7 +758,7 @@ function runProjectCommand(rootDir, command, arg) {
739
758
  }
740
759
 
741
760
  case 'tests': {
742
- const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className });
761
+ const { ok, result, error } = execute(index, 'tests', { name: arg, callsOnly: flags.callsOnly, className: flags.className, file: flags.file, exclude: flags.exclude });
743
762
  if (!ok) fail(error);
744
763
  printOutput(result,
745
764
  r => output.formatTestsJson(r, arg),
@@ -912,31 +931,60 @@ function runGlobCommand(pattern, command, arg) {
912
931
  const index = new ProjectIndex(rootDir);
913
932
  index.build(files, { quiet: true });
914
933
 
915
- // Supported commands — anything that works with an index
916
- const supportedGlobCommands = new Set(['toc', 'find', 'search', 'fn', 'class', 'usages', 'deadcode', 'typedef', 'stats']);
917
- if (!supportedGlobCommands.has(canonical)) {
918
- console.error(`Command "${command}" not supported in glob mode. Supported: ${[...supportedGlobCommands].join(', ')}`);
934
+ // Supported commands — anything that works with an index.
935
+ // All execute() commands are supported; only expand (requires cached state)
936
+ // and interactive-only commands are excluded.
937
+ const unsupportedGlobCommands = new Set(['expand']);
938
+ if (unsupportedGlobCommands.has(canonical)) {
939
+ console.error(`Command "${command}" not supported in glob mode.`);
919
940
  process.exit(1);
920
941
  }
921
942
 
922
943
  // Build params — same as project mode
923
944
  const params = {};
924
- if (['find', 'usages', 'fn', 'class', 'typedef'].includes(canonical)) {
945
+ const needsName = new Set(['find', 'usages', 'fn', 'class', 'typedef', 'about', 'context',
946
+ 'smart', 'impact', 'trace', 'blast', 'reverseTrace', 'tests', 'affectedTests',
947
+ 'example', 'verify', 'plan', 'related']);
948
+ if (needsName.has(canonical)) {
925
949
  if (!arg) {
926
950
  console.error(`Usage: ucn "pattern" ${command} <name>`);
927
951
  process.exit(1);
928
952
  }
929
953
  params.name = arg;
930
954
  }
931
- if (canonical === 'search') {
955
+ if (canonical === 'search' || canonical === 'structuralSearch') {
932
956
  if (!arg && !flags.type) {
933
957
  console.error('Usage: ucn "pattern" search <term>');
934
958
  process.exit(1);
935
959
  }
936
960
  params.term = arg;
937
961
  }
962
+ // Merge flags first, then set positional overrides so they aren't wiped
938
963
  Object.assign(params, flags);
939
964
 
965
+ // Warn about inapplicable flags (same check as project/interactive mode)
966
+ const applicableFlags = FLAG_APPLICABILITY[canonical];
967
+ if (applicableFlags) {
968
+ const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
969
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
970
+ for (const [key, value] of Object.entries(flags)) {
971
+ if (globalFlags.has(key)) continue;
972
+ if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
973
+ if (!applicableFlags.includes(key)) {
974
+ console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
975
+ }
976
+ }
977
+ }
978
+ if (canonical === 'stacktrace' && arg) {
979
+ params.stack = arg;
980
+ }
981
+ if (canonical === 'lines' && arg) {
982
+ params.range = arg;
983
+ }
984
+ if (['imports', 'exporters', 'fileExports', 'graph', 'api'].includes(canonical)) {
985
+ if (arg) params.file = arg;
986
+ }
987
+
940
988
  const { ok, result, error, note, structural } = execute(index, canonical, params);
941
989
  if (!ok) fail(error);
942
990
  if (note) console.error(note);
@@ -982,6 +1030,109 @@ function runGlobCommand(pattern, command, arg) {
982
1030
  case 'stats':
983
1031
  printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
984
1032
  break;
1033
+ case 'about':
1034
+ printOutput(result, output.formatAboutJson,
1035
+ r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence !== false }));
1036
+ break;
1037
+ case 'context':
1038
+ if (flags.json) {
1039
+ console.log(output.formatContextJson(result));
1040
+ } else {
1041
+ const { text } = output.formatContext(result, {
1042
+ methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1043
+ uncertainHint: 'use --include-uncertain to include all',
1044
+ expandHint: 'Use --expand to see inline callee previews',
1045
+ showConfidence: flags.showConfidence !== false,
1046
+ });
1047
+ console.log(text);
1048
+ if (flags.expand) {
1049
+ printInlineExpand(result, index.root);
1050
+ }
1051
+ }
1052
+ break;
1053
+ case 'smart':
1054
+ printOutput(result, output.formatSmartJson, output.formatSmart);
1055
+ break;
1056
+ case 'impact':
1057
+ printOutput(result, output.formatImpactJson, output.formatImpact);
1058
+ break;
1059
+ case 'related':
1060
+ printOutput(result, output.formatRelatedJson,
1061
+ r => output.formatRelated(r, { all: flags.all, top: flags.top }));
1062
+ break;
1063
+ case 'trace':
1064
+ printOutput(result, output.formatTraceJson, output.formatTrace);
1065
+ break;
1066
+ case 'blast':
1067
+ printOutput(result, output.formatBlastJson, output.formatBlast);
1068
+ break;
1069
+ case 'reverseTrace':
1070
+ printOutput(result, output.formatReverseTraceJson, output.formatReverseTrace);
1071
+ break;
1072
+ case 'tests':
1073
+ printOutput(result, r => output.formatTestsJson(r, arg), r => output.formatTests(r, arg));
1074
+ break;
1075
+ case 'affectedTests':
1076
+ printOutput(result, output.formatAffectedTestsJson,
1077
+ r => output.formatAffectedTests(r, { all: flags.all }));
1078
+ break;
1079
+ case 'example':
1080
+ printOutput(result, r => output.formatExampleJson(r, arg), r => output.formatExample(r, arg));
1081
+ break;
1082
+ case 'verify':
1083
+ printOutput(result, output.formatVerifyJson, output.formatVerify);
1084
+ break;
1085
+ case 'plan':
1086
+ printOutput(result, output.formatPlanJson, output.formatPlan);
1087
+ break;
1088
+ case 'imports': {
1089
+ const filePath = params.file;
1090
+ printOutput(result, r => output.formatImportsJson(r, filePath), r => output.formatImports(r, filePath));
1091
+ break;
1092
+ }
1093
+ case 'exporters': {
1094
+ const filePath = params.file;
1095
+ printOutput(result, r => output.formatExportersJson(r, filePath), r => output.formatExporters(r, filePath));
1096
+ break;
1097
+ }
1098
+ case 'fileExports': {
1099
+ const filePath = params.file;
1100
+ printOutput(result, r => output.formatFileExportsJson(r, filePath), r => output.formatFileExports(r, filePath));
1101
+ break;
1102
+ }
1103
+ case 'api': {
1104
+ const filePath = params.file;
1105
+ printOutput(result, r => output.formatApiJson(r, filePath), r => output.formatApi(r, filePath));
1106
+ break;
1107
+ }
1108
+ case 'graph':
1109
+ printOutput(result, output.formatGraphJson,
1110
+ r => output.formatGraph(r, { showAll: flags.all || flags.depth != null, maxDepth: flags.depth }));
1111
+ break;
1112
+ case 'circularDeps':
1113
+ printOutput(result, output.formatCircularDepsJson, output.formatCircularDeps);
1114
+ break;
1115
+ case 'entrypoints':
1116
+ printOutput(result, output.formatEntrypointsJson, output.formatEntrypoints);
1117
+ break;
1118
+ case 'diffImpact':
1119
+ printOutput(result, output.formatDiffImpactJson, output.formatDiffImpact);
1120
+ break;
1121
+ case 'stacktrace':
1122
+ printOutput(result, output.formatStackTraceJson, output.formatStackTrace);
1123
+ break;
1124
+ case 'lines':
1125
+ printOutput(result, output.formatLinesJson, output.formatLines);
1126
+ break;
1127
+ default: {
1128
+ // Fallback: output JSON for any command without a dedicated formatter
1129
+ if (flags.json) {
1130
+ console.log(JSON.stringify({ meta: {}, data: result }, null, 2));
1131
+ } else {
1132
+ console.log(JSON.stringify(result, null, 2));
1133
+ }
1134
+ break;
1135
+ }
985
1136
  }
986
1137
  }
987
1138
 
@@ -1023,7 +1174,7 @@ FIND CODE
1023
1174
  toc Table of contents (compact; --detailed lists all symbols)
1024
1175
  search <term> Text search (regex default, --context=N, --exclude=, --in=)
1025
1176
  Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
1026
- tests <name> Find test files for a function
1177
+ tests <name> Find test files for a function (--file, --class-name, --exclude, --calls-only)
1027
1178
  affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
1028
1179
 
1029
1180
  ═══════════════════════════════════════════════════════════════════════════════
@@ -1076,7 +1227,7 @@ Common Flags:
1076
1227
  --code-only Filter out comments/strings (search, usages)
1077
1228
  --with-types Include type definitions (about, smart)
1078
1229
  --detailed Show all symbols in toc (not just counts)
1079
- --include-tests Include test files
1230
+ --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1080
1231
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1081
1232
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1082
1233
  --include-uncertain Include ambiguous/uncertain matches
@@ -1171,7 +1322,7 @@ Commands:
1171
1322
  file-exports <file> File's exported symbols
1172
1323
  imports <file> What file imports
1173
1324
  exporters <file> Who imports file
1174
- tests <name> Find tests (--calls-only)
1325
+ tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
1175
1326
  affected-tests <n> Tests affected by a change (--depth=N)
1176
1327
  search <term> Text search (--context=N, --exclude=, --in=)
1177
1328
  Structural: --type= --param= --returns= --decorator= --exported --unused
@@ -1255,7 +1406,7 @@ Flags can be added per-command: context myFunc --include-methods
1255
1406
 
1256
1407
  const INTERACTIVE_DISPATCH = {
1257
1408
  // ── Understanding Code ───────────────────────────────────────────
1258
- 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 }) },
1409
+ about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false }) },
1259
1410
  smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
1260
1411
  impact: { params: 'name', format: (r) => output.formatImpact(r) },
1261
1412
  blast: { params: 'name', format: (r) => output.formatBlast(r) },
@@ -1309,6 +1460,20 @@ function buildInteractiveParams(descriptor, arg, iflags) {
1309
1460
  }
1310
1461
 
1311
1462
  function executeInteractiveCommand(index, command, arg, iflags = {}, cache = null) {
1463
+ // Warn about inapplicable flags (same check as project mode)
1464
+ const applicableFlags = FLAG_APPLICABILITY[command];
1465
+ if (applicableFlags) {
1466
+ const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
1467
+ const globalFlags = new Set(['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'maxFiles', 'verbose', 'expand', 'interactive', '_fileFromFileMode']);
1468
+ for (const [key, value] of Object.entries(iflags)) {
1469
+ if (globalFlags.has(key)) continue;
1470
+ if (value === undefined || value === null || value === 0 || (Array.isArray(value) && value.length === 0)) continue;
1471
+ if (!applicableFlags.includes(key)) {
1472
+ console.log(`Warning: ${flagToCli(key)} has no effect on '${command}'.`);
1473
+ }
1474
+ }
1475
+ }
1476
+
1312
1477
  // ── Commands with unique behavior (not data-driven) ──────────────
1313
1478
  switch (command) {
1314
1479
 
@@ -1375,9 +1540,12 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1375
1540
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1376
1541
  expandHint: 'Use "expand <N>" to see code for item N',
1377
1542
  uncertainHint: 'use --include-uncertain to include all',
1378
- showConfidence: iflags.showConfidence,
1543
+ showConfidence: iflags.showConfidence !== false,
1379
1544
  });
1380
1545
  console.log(text);
1546
+ if (iflags.expand) {
1547
+ printInlineExpand(result, index.root);
1548
+ }
1381
1549
  if (note) console.log(note);
1382
1550
  if (cache) {
1383
1551
  cache.save(index.root, arg, iflags.file, expandable);
package/core/analysis.js CHANGED
@@ -836,8 +836,12 @@ function about(index, name, options = {}) {
836
836
  }));
837
837
  }
838
838
 
839
- // Find tests
840
- const tests = index.tests(symbolName);
839
+ // Find tests — scope to the same file/class as the primary definition
840
+ const tests = index.tests(symbolName, {
841
+ file: options.file,
842
+ className: options.className || primary.className,
843
+ exclude: options.exclude,
844
+ });
841
845
  const testSummary = {
842
846
  fileCount: tests.length,
843
847
  totalMatches: tests.reduce((sum, t) => sum + t.matches.length, 0),
package/core/execute.js CHANGED
@@ -96,6 +96,7 @@ function buildCallerOptions(p) {
96
96
  className: p.className,
97
97
  includeMethods: p.includeMethods,
98
98
  includeUncertain: p.includeUncertain || false,
99
+ includeTests: p.includeTests,
99
100
  exclude: toExcludeArray(p.exclude),
100
101
  minConfidence: num(p.minConfidence, 0),
101
102
  };
@@ -592,11 +593,27 @@ const HANDLERS = {
592
593
  const err = requireName(p.name);
593
594
  if (err) return { ok: false, error: err };
594
595
  applyClassMethodSyntax(p);
596
+ if (p.file) {
597
+ const fileErr = checkFilePatternMatch(index, p.file);
598
+ if (fileErr) return { ok: false, error: fileErr };
599
+ // Validate that the symbol exists in the target file
600
+ const defs = index.find(p.name, { exact: true, file: p.file, className: p.className });
601
+ if (defs.length === 0) {
602
+ const allDefs = index.find(p.name, { exact: true });
603
+ if (allDefs.length > 0) {
604
+ const files = allDefs.map(d => d.relativePath).join(', ');
605
+ return { ok: false, error: `Symbol "${p.name}" not found in files matching "${p.file}". Defined in: ${files}` };
606
+ }
607
+ return { ok: false, error: `Symbol "${p.name}" not found.` };
608
+ }
609
+ }
595
610
  const classErr = validateClassName(index, p.name, p.className);
596
611
  if (classErr) return { ok: false, error: classErr };
597
612
  const result = index.tests(p.name, {
598
613
  callsOnly: p.callsOnly || false,
599
614
  className: p.className,
615
+ file: p.file,
616
+ exclude: toExcludeArray(p.exclude),
600
617
  });
601
618
  return { ok: true, result };
602
619
  },
@@ -80,7 +80,7 @@ function formatContextJson(context) {
80
80
  function formatContext(ctx, options = {}) {
81
81
  if (!ctx) return { text: 'Symbol not found.', expandable: [] };
82
82
 
83
- const expandHint = options.expandHint || 'Use ucn_expand with item number to see code for any item.';
83
+ const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
84
84
  const methodsHint = options.methodsHint || 'Note: obj.method() calls excluded. Use include_methods=true to include them.';
85
85
 
86
86
  const lines = [];
package/core/registry.js CHANGED
@@ -92,45 +92,45 @@ const PARAM_MAP = {
92
92
  // ============================================================================
93
93
 
94
94
  // Per-command list of accepted flag names (camelCase). Source of truth for help text,
95
- // MCP schema validation, and architecture guards.
95
+ // MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
96
96
  // file* = file is the command subject (required), not a filter pattern.
97
97
  const FLAG_APPLICABILITY = {
98
98
  // Understanding code
99
- about: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
100
- context: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
101
- impact: ['file', 'exclude', 'className', 'top'],
102
- blast: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
103
- reverseTrace: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
104
- smart: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
105
- trace: ['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
106
- example: ['file', 'className'],
107
- related: ['file', 'className', 'top', 'all'],
99
+ about: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence'],
100
+ context: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence'],
101
+ impact: ['name', 'file', 'exclude', 'className', 'top'],
102
+ blast: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
103
+ reverseTrace: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
104
+ smart: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
105
+ trace: ['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
106
+ example: ['name', 'file', 'className'],
107
+ related: ['name', 'file', 'className', 'top', 'all'],
108
108
  // Finding code
109
- find: ['file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all', 'depth'],
110
- usages: ['file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
109
+ find: ['name', 'file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all', 'depth'],
110
+ usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in'],
111
111
  toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
112
- search: ['file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
113
- tests: ['className', 'callsOnly'],
114
- affectedTests:['file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
112
+ search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
113
+ tests: ['name', 'file', 'exclude', 'className', 'callsOnly'],
114
+ affectedTests:['name', 'file', 'exclude', 'className', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
115
115
  deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
116
116
  entrypoints: ['file', 'exclude', 'includeTests', 'limit', 'type', 'framework'],
117
117
  // Extracting code
118
- fn: ['file', 'className', 'all'],
119
- class: ['file', 'all', 'maxLines'],
118
+ fn: ['name', 'file', 'className', 'all'],
119
+ class: ['name', 'file', 'all', 'maxLines'],
120
120
  lines: ['file', 'range'],
121
- expand: [],
121
+ expand: ['item'],
122
122
  // File dependencies
123
123
  imports: ['file'],
124
124
  exporters: ['file'],
125
125
  fileExports: ['file'],
126
- graph: ['file', 'depth', 'direction'],
126
+ graph: ['file', 'depth', 'direction', 'all'],
127
127
  circularDeps: ['file', 'exclude'],
128
128
  // Refactoring
129
- verify: ['file', 'className'],
130
- plan: ['file', 'className', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
129
+ verify: ['name', 'file', 'className'],
130
+ plan: ['name', 'file', 'className', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
131
131
  diffImpact: ['file', 'limit', 'base', 'staged', 'all'],
132
132
  // Other
133
- typedef: ['file', 'className', 'exact'],
133
+ typedef: ['name', 'file', 'className', 'exact'],
134
134
  stacktrace: ['stack'],
135
135
  api: ['file', 'limit'],
136
136
  stats: ['functions', 'top'],
@@ -224,11 +224,43 @@ function toCliName(canonical) {
224
224
  return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
225
225
  }
226
226
 
227
+ /**
228
+ * Build a reverse map: camelCase → snake_case from PARAM_MAP.
229
+ * Flags not in PARAM_MAP are already snake_case-safe (single words).
230
+ */
231
+ function buildReverseParamMap() {
232
+ const rev = {};
233
+ for (const [snake, camel] of Object.entries(PARAM_MAP)) {
234
+ rev[camel] = snake;
235
+ }
236
+ return rev;
237
+ }
238
+
239
+ const REVERSE_PARAM_MAP = buildReverseParamMap();
240
+
241
+ /**
242
+ * Generate per-command parameter listing for the MCP tool description.
243
+ * Maps camelCase flags back to snake_case for MCP clients.
244
+ * One line per command: `about: file, exclude, class_name, ...`
245
+ */
246
+ function generateMcpParamSection() {
247
+ const lines = ['', 'ACCEPTED FLAGS PER COMMAND (max_chars, max_files, follow_symlinks always accepted; flags not listed below are ignored):'];
248
+ for (const cmd of CANONICAL_COMMANDS) {
249
+ const flags = FLAG_APPLICABILITY[cmd];
250
+ if (!flags || flags.length === 0) continue;
251
+ const mcpCmd = toMcpName(cmd);
252
+ const mcpFlags = flags.map(f => REVERSE_PARAM_MAP[f] || f);
253
+ lines.push(` ${mcpCmd}: ${mcpFlags.join(', ')}`);
254
+ }
255
+ return lines.join('\n');
256
+ }
257
+
227
258
  module.exports = {
228
259
  CANONICAL_COMMANDS,
229
260
  CLI_ALIASES,
230
261
  MCP_ALIASES,
231
262
  PARAM_MAP,
263
+ REVERSE_PARAM_MAP,
232
264
  FLAG_APPLICABILITY,
233
265
  BROAD_COMMANDS,
234
266
  resolveCommand,
@@ -237,4 +269,5 @@ module.exports = {
237
269
  getMcpCommandEnum,
238
270
  toMcpName,
239
271
  toCliName,
272
+ generateMcpParamSection,
240
273
  };