ucn 3.8.17 → 3.8.19

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.
package/cli/index.js CHANGED
@@ -10,11 +10,10 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
- const { parseFile, detectLanguage } = require('../core/parser');
13
+ 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 { escapeRegExp } = require('../core/shared');
18
17
  const { getCliCommandSet, resolveCommand, FLAG_APPLICABILITY, toCliName } = require('../core/registry');
19
18
  const { execute } = require('../core/execute');
20
19
  const { ExpandCache } = require('../core/expand-cache');
@@ -75,19 +74,19 @@ function parseFlags(tokens) {
75
74
  exclude: parseExclude(),
76
75
  in: getValueFlag('--in'),
77
76
  includeTests: tokens.includes('--include-tests') ? true : undefined,
78
- includeExported: tokens.includes('--include-exported'),
79
- includeDecorated: tokens.includes('--include-decorated'),
80
- 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,
81
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,
82
- detailed: tokens.includes('--detailed'),
83
- topLevel: tokens.includes('--top-level'),
84
- all: tokens.includes('--all'),
85
- exact: tokens.includes('--exact'),
86
- callsOnly: tokens.includes('--calls-only'),
87
- codeOnly: tokens.includes('--code-only'),
88
- caseSensitive: tokens.includes('--case-sensitive'),
89
- withTypes: tokens.includes('--with-types'),
90
- 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,
91
90
  depth: getValueFlag('--depth'),
92
91
  top: parseInt(getValueFlag('--top') || '0'),
93
92
  context: parseInt(getValueFlag('--context') || '0'),
@@ -97,10 +96,10 @@ function parseFlags(tokens) {
97
96
  renameTo: getValueFlag('--rename-to'),
98
97
  defaultValue: getValueFlag('--default'),
99
98
  base: getValueFlag('--base'),
100
- staged: tokens.includes('--staged'),
99
+ staged: tokens.includes('--staged') || undefined,
101
100
  maxLines: getValueFlag('--max-lines') || null,
102
101
  regex: tokens.includes('--no-regex') ? false : undefined,
103
- functions: tokens.includes('--functions'),
102
+ functions: tokens.includes('--functions') || undefined,
104
103
  className: getValueFlag('--class-name'),
105
104
  limit: parseInt(getValueFlag('--limit') || '0') || undefined,
106
105
  maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
@@ -110,9 +109,9 @@ function parseFlags(tokens) {
110
109
  receiver: getValueFlag('--receiver'),
111
110
  returns: getValueFlag('--returns'),
112
111
  decorator: getValueFlag('--decorator'),
113
- exported: tokens.includes('--exported'),
114
- unused: tokens.includes('--unused'),
115
- 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,
116
115
  minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
117
116
  framework: getValueFlag('--framework'),
118
117
  stack: getValueFlag('--stack'),
@@ -228,6 +227,33 @@ function printOutput(result, jsonFn, textFn) {
228
227
  }
229
228
  }
230
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
+
231
257
  // ============================================================================
232
258
  // MAIN
233
259
  // ============================================================================
@@ -305,6 +331,12 @@ function runFileCommand(filePath, command, arg) {
305
331
  if (['imports', 'exporters', 'fileExports', 'graph'].includes(canonical) && !arg) {
306
332
  effectiveArg = filePath;
307
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
+ }
308
340
  runProjectCommand(projectRoot, command, effectiveArg);
309
341
  return;
310
342
  }
@@ -375,9 +407,11 @@ function runFileCommand(filePath, command, arg) {
375
407
  case 'typedef':
376
408
  printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
377
409
  break;
378
- case 'api':
379
- 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));
380
413
  break;
414
+ }
381
415
  }
382
416
  }
383
417
 
@@ -444,12 +478,14 @@ function runProjectCommand(rootDir, command, arg) {
444
478
  if (applicableFlags) {
445
479
  // Map from camelCase flag name to CLI flag string
446
480
  const flagToCli = (f) => '--' + f.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
447
- // Flags that are global (not command-specific) or have truthy defaults — skip warning for these
448
- 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']);
449
483
  for (const [key, value] of Object.entries(flags)) {
450
484
  if (globalFlags.has(key)) continue;
451
- // Skip falsy/default values (0, undefined, false, empty array)
452
- 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;
453
489
  if (!applicableFlags.includes(key)) {
454
490
  console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
455
491
  }
@@ -510,33 +546,15 @@ function runProjectCommand(rootDir, command, arg) {
510
546
  } else {
511
547
  const { text, expandable } = output.formatContext(ctx, {
512
548
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
513
- expandHint: 'Use "ucn . expand <N>" to see code for item N',
549
+ expandHint: 'Use "expand <N>" or --expand to see code for items',
514
550
  uncertainHint: 'use --include-uncertain to include all',
515
- showConfidence: flags.showConfidence,
551
+ showConfidence: flags.showConfidence !== false,
516
552
  });
517
553
  console.log(text);
518
554
 
519
555
  // Inline expansion of callees when --expand flag is set
520
- if (flags.expand && index.root && ctx.callees) {
521
- for (const c of ctx.callees) {
522
- if (c.relativePath && c.startLine) {
523
- try {
524
- const filePath = path.join(index.root, c.relativePath);
525
- const content = fs.readFileSync(filePath, 'utf-8');
526
- const codeLines = content.split('\n');
527
- const endLine = c.endLine || c.startLine + 5;
528
- const previewLines = Math.min(3, endLine - c.startLine + 1);
529
- for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
530
- console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
531
- }
532
- if (endLine - c.startLine + 1 > 3) {
533
- console.log(` │ ... (${endLine - c.startLine - 2} more lines)`);
534
- }
535
- } catch (e) {
536
- // Skip on error
537
- }
538
- }
539
- }
556
+ if (flags.expand) {
557
+ printInlineExpand(ctx, index.root);
540
558
  }
541
559
 
542
560
  // Save expandable items to cache for 'expand' command
@@ -578,7 +596,7 @@ function runProjectCommand(rootDir, command, arg) {
578
596
  if (!ok) fail(error);
579
597
  printOutput(result,
580
598
  output.formatAboutJson,
581
- 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 })
582
600
  );
583
601
  if (note) console.error(note);
584
602
  break;
@@ -740,7 +758,7 @@ function runProjectCommand(rootDir, command, arg) {
740
758
  }
741
759
 
742
760
  case 'tests': {
743
- 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 });
744
762
  if (!ok) fail(error);
745
763
  printOutput(result,
746
764
  r => output.formatTestsJson(r, arg),
@@ -904,164 +922,217 @@ function runGlobCommand(pattern, command, arg) {
904
922
  process.exit(1);
905
923
  }
906
924
 
907
- switch (command) {
908
- case 'toc': {
909
- let totalFunctions = 0;
910
- let totalClasses = 0;
911
- let totalState = 0;
912
- let totalLines = 0;
913
- const byFile = [];
914
-
915
- for (const file of files) {
916
- try {
917
- const result = parseFile(file);
918
- let functions = result.functions;
919
- if (flags.topLevel) {
920
- functions = functions.filter(fn => !fn.isNested && (!fn.indent || fn.indent === 0));
921
- }
922
- totalFunctions += functions.length;
923
- totalClasses += result.classes.length;
924
- totalState += result.stateObjects.length;
925
- totalLines += result.totalLines;
926
- byFile.push({
927
- file,
928
- language: result.language,
929
- lines: result.totalLines,
930
- functions,
931
- classes: result.classes,
932
- state: result.stateObjects
933
- });
934
- } catch (e) {
935
- // Skip unparseable files
936
- }
937
- }
938
-
939
- // Convert glob toc to shared formatter format
940
- const toc = {
941
- totals: { files: files.length, lines: totalLines, functions: totalFunctions, classes: totalClasses, state: totalState },
942
- files: byFile.map(f => ({
943
- file: f.file,
944
- lines: f.lines,
945
- functions: f.functions.length,
946
- classes: f.classes.length,
947
- state: f.stateObjects ? f.stateObjects.length : (f.state ? f.state.length : 0)
948
- })),
949
- meta: {}
950
- };
951
- if (flags.json) {
952
- console.log(output.formatTocJson(toc));
953
- } else {
954
- console.log(output.formatToc(toc, {
955
- detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
956
- }));
957
- }
958
- break;
959
- }
960
-
961
- case 'find':
962
- if (!arg) {
963
- console.error('Usage: ucn "pattern" find <name>');
964
- process.exit(1);
965
- }
966
- findInGlobFiles(files, arg);
967
- break;
925
+ const canonical = resolveCommand(command, 'cli') || command;
968
926
 
969
- case 'search':
970
- if (!arg) {
971
- console.error('Usage: ucn "pattern" search <term>');
972
- process.exit(1);
973
- }
974
- searchGlobFiles(files, arg);
975
- break;
927
+ // Build a temporary index over the matched files and route through execute().
928
+ // This gives glob mode the same semantics as project mode: test exclusions,
929
+ // limit, all flags — no bespoke logic, no parity drift.
930
+ const rootDir = findProjectRoot(path.dirname(files[0]));
931
+ const index = new ProjectIndex(rootDir);
932
+ index.build(files, { quiet: true });
933
+
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.`);
940
+ process.exit(1);
941
+ }
976
942
 
977
- default:
978
- console.error(`Command "${command}" not supported in glob mode`);
943
+ // Build params — same as project mode
944
+ const params = {};
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)) {
949
+ if (!arg) {
950
+ console.error(`Usage: ucn "pattern" ${command} <name>`);
979
951
  process.exit(1);
952
+ }
953
+ params.name = arg;
980
954
  }
981
- }
982
-
983
- function findInGlobFiles(files, name) {
984
- const allMatches = [];
985
- const lowerName = name.toLowerCase();
986
-
987
- for (const file of files) {
988
- try {
989
- const result = parseFile(file);
990
-
991
- for (const fn of result.functions) {
992
- if (flags.exact ? fn.name === name : fn.name.toLowerCase().includes(lowerName)) {
993
- allMatches.push({ ...fn, type: 'function', relativePath: file });
994
- }
995
- }
955
+ if (canonical === 'search' || canonical === 'structuralSearch') {
956
+ if (!arg && !flags.type) {
957
+ console.error('Usage: ucn "pattern" search <term>');
958
+ process.exit(1);
959
+ }
960
+ params.term = arg;
961
+ }
962
+ // Merge flags first, then set positional overrides so they aren't wiped
963
+ Object.assign(params, flags);
996
964
 
997
- for (const cls of result.classes) {
998
- if (flags.exact ? cls.name === name : cls.name.toLowerCase().includes(lowerName)) {
999
- allMatches.push({ ...cls, relativePath: file });
1000
- }
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)}'.`);
1001
975
  }
1002
- } catch (e) {
1003
- // Skip
1004
976
  }
1005
977
  }
1006
-
1007
- if (flags.json) {
1008
- console.log(output.formatSymbolJson(allMatches, name));
1009
- } else {
1010
- console.log(output.formatFindDetailed(allMatches, name, { depth: flags.depth, top: flags.top, all: flags.all }));
978
+ if (canonical === 'stacktrace' && arg) {
979
+ params.stack = arg;
1011
980
  }
1012
- }
1013
-
1014
- function searchGlobFiles(files, term) {
1015
- const results = [];
1016
- const useRegex = flags.regex !== false; // Default: regex ON
1017
- let regex;
1018
- if (useRegex) {
1019
- try { regex = new RegExp(term, flags.caseSensitive ? '' : 'i'); } catch (e) { regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i'); }
1020
- } else {
1021
- regex = new RegExp(escapeRegExp(term), flags.caseSensitive ? '' : 'i');
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;
1022
986
  }
1023
987
 
1024
- for (const file of files) {
1025
- try {
1026
- const content = fs.readFileSync(file, 'utf-8');
1027
- const lines = content.split('\n');
1028
- const matches = [];
1029
-
1030
- lines.forEach((line, idx) => {
1031
- if (regex.test(line)) {
1032
- if (flags.codeOnly && isCommentOrString(line)) {
1033
- return;
1034
- }
1035
-
1036
- const match = { line: idx + 1, content: line };
1037
-
1038
- if (flags.context > 0) {
1039
- const before = [];
1040
- const after = [];
1041
- for (let i = 1; i <= flags.context; i++) {
1042
- if (idx - i >= 0) before.unshift(lines[idx - i]);
1043
- if (idx + i < lines.length) after.push(lines[idx + i]);
1044
- }
1045
- match.before = before;
1046
- match.after = after;
1047
- }
1048
-
1049
- matches.push(match);
1050
- }
1051
- });
988
+ const { ok, result, error, note, structural } = execute(index, canonical, params);
989
+ if (!ok) fail(error);
990
+ if (note) console.error(note);
1052
991
 
1053
- if (matches.length > 0) {
1054
- results.push({ file, matches });
992
+ // Format output same formatters as project mode
993
+ switch (canonical) {
994
+ case 'toc':
995
+ printOutput(result, output.formatTocJson, r => output.formatToc(r, {
996
+ detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol'
997
+ }));
998
+ break;
999
+ case 'find':
1000
+ printOutput(result,
1001
+ r => output.formatSymbolJson(r, arg),
1002
+ r => output.formatFindDetailed(r, arg, { depth: flags.depth, top: flags.top, all: flags.all })
1003
+ );
1004
+ break;
1005
+ case 'search':
1006
+ if (structural) {
1007
+ printOutput(result, output.formatStructuralSearchJson, output.formatStructuralSearch);
1008
+ } else {
1009
+ printOutput(result,
1010
+ r => output.formatSearchJson(r, arg),
1011
+ r => output.formatSearch(r, arg)
1012
+ );
1055
1013
  }
1056
- } catch (e) {
1057
- // Skip
1014
+ break;
1015
+ case 'fn':
1016
+ printOutput(result, output.formatFnResultJson, output.formatFnResult);
1017
+ break;
1018
+ case 'class':
1019
+ printOutput(result, output.formatClassResultJson, output.formatClassResult);
1020
+ break;
1021
+ case 'usages':
1022
+ printOutput(result, r => output.formatUsagesJson(r, arg), r => output.formatUsages(r, arg));
1023
+ break;
1024
+ case 'deadcode':
1025
+ printOutput(result, output.formatDeadcodeJson, r => output.formatDeadcode(r, { top: flags.top }));
1026
+ break;
1027
+ case 'typedef':
1028
+ printOutput(result, r => output.formatTypedefJson(r, arg), r => output.formatTypedef(r, arg));
1029
+ break;
1030
+ case 'stats':
1031
+ printOutput(result, output.formatStatsJson, r => output.formatStats(r, { top: flags.top }));
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;
1058
1135
  }
1059
- }
1060
-
1061
- if (flags.json) {
1062
- console.log(output.formatSearchJson(results, term));
1063
- } else {
1064
- console.log(output.formatSearch(results, term));
1065
1136
  }
1066
1137
  }
1067
1138
 
@@ -1069,13 +1140,6 @@ function searchGlobFiles(files, term) {
1069
1140
  // HELPERS
1070
1141
  // ============================================================================
1071
1142
 
1072
- function isCommentOrString(line) {
1073
- const trimmed = line.trim();
1074
- return trimmed.startsWith('//') ||
1075
- trimmed.startsWith('#') ||
1076
- trimmed.startsWith('*') ||
1077
- trimmed.startsWith('/*');
1078
- }
1079
1143
 
1080
1144
  function printUsage() {
1081
1145
  console.log(`UCN - Universal Code Navigator
@@ -1110,7 +1174,7 @@ FIND CODE
1110
1174
  toc Table of contents (compact; --detailed lists all symbols)
1111
1175
  search <term> Text search (regex default, --context=N, --exclude=, --in=)
1112
1176
  Structural: --type=function|class|call --param= --returns= --decorator= --exported --unused
1113
- tests <name> Find test files for a function
1177
+ tests <name> Find test files for a function (--file, --class-name, --exclude, --calls-only)
1114
1178
  affected-tests <n> Tests affected by a change (blast + test detection, --depth=N)
1115
1179
 
1116
1180
  ═══════════════════════════════════════════════════════════════════════════════
@@ -1163,7 +1227,7 @@ Common Flags:
1163
1227
  --code-only Filter out comments/strings (search, usages)
1164
1228
  --with-types Include type definitions (about, smart)
1165
1229
  --detailed Show all symbols in toc (not just counts)
1166
- --include-tests Include test files
1230
+ --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1167
1231
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1168
1232
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1169
1233
  --include-uncertain Include ambiguous/uncertain matches
@@ -1258,7 +1322,7 @@ Commands:
1258
1322
  file-exports <file> File's exported symbols
1259
1323
  imports <file> What file imports
1260
1324
  exporters <file> Who imports file
1261
- tests <name> Find tests (--calls-only)
1325
+ tests <name> Find tests (--file, --class-name, --exclude, --calls-only)
1262
1326
  affected-tests <n> Tests affected by a change (--depth=N)
1263
1327
  search <term> Text search (--context=N, --exclude=, --in=)
1264
1328
  Structural: --type= --param= --returns= --decorator= --exported --unused
@@ -1342,7 +1406,7 @@ Flags can be added per-command: context myFunc --include-methods
1342
1406
 
1343
1407
  const INTERACTIVE_DISPATCH = {
1344
1408
  // ── Understanding Code ───────────────────────────────────────────
1345
- 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 }) },
1346
1410
  smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
1347
1411
  impact: { params: 'name', format: (r) => output.formatImpact(r) },
1348
1412
  blast: { params: 'name', format: (r) => output.formatBlast(r) },
@@ -1396,6 +1460,20 @@ function buildInteractiveParams(descriptor, arg, iflags) {
1396
1460
  }
1397
1461
 
1398
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
+
1399
1477
  // ── Commands with unique behavior (not data-driven) ──────────────
1400
1478
  switch (command) {
1401
1479
 
@@ -1462,9 +1540,12 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1462
1540
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1463
1541
  expandHint: 'Use "expand <N>" to see code for item N',
1464
1542
  uncertainHint: 'use --include-uncertain to include all',
1465
- showConfidence: iflags.showConfidence,
1543
+ showConfidence: iflags.showConfidence !== false,
1466
1544
  });
1467
1545
  console.log(text);
1546
+ if (iflags.expand) {
1547
+ printInlineExpand(result, index.root);
1548
+ }
1468
1549
  if (note) console.log(note);
1469
1550
  if (cache) {
1470
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),