ucn 4.0.2 → 4.1.0

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 (46) hide show
  1. package/.claude/skills/ucn/SKILL.md +31 -7
  2. package/README.md +89 -50
  3. package/cli/index.js +199 -94
  4. package/core/account.js +3 -1
  5. package/core/analysis.js +322 -304
  6. package/core/bridge.js +13 -8
  7. package/core/cache.js +109 -19
  8. package/core/callers.js +969 -77
  9. package/core/check.js +19 -8
  10. package/core/deadcode.js +368 -40
  11. package/core/discovery.js +31 -11
  12. package/core/entrypoints.js +149 -17
  13. package/core/execute.js +330 -43
  14. package/core/graph-build.js +61 -10
  15. package/core/graph.js +282 -61
  16. package/core/imports.js +72 -3
  17. package/core/output/analysis-ext.js +70 -10
  18. package/core/output/analysis.js +52 -33
  19. package/core/output/check.js +4 -1
  20. package/core/output/endpoints.js +8 -3
  21. package/core/output/extraction.js +12 -1
  22. package/core/output/find.js +23 -9
  23. package/core/output/graph.js +32 -9
  24. package/core/output/refactoring.js +147 -49
  25. package/core/output/reporting.js +104 -5
  26. package/core/output/search.js +30 -3
  27. package/core/output/shared.js +31 -4
  28. package/core/output/tracing.js +22 -11
  29. package/core/parser.js +8 -6
  30. package/core/project.js +167 -7
  31. package/core/registry.js +20 -16
  32. package/core/reporting.js +131 -13
  33. package/core/search.js +270 -55
  34. package/core/shared.js +240 -1
  35. package/core/stacktrace.js +23 -1
  36. package/core/tracing.js +278 -36
  37. package/core/verify.js +352 -349
  38. package/languages/go.js +29 -17
  39. package/languages/index.js +56 -0
  40. package/languages/java.js +133 -16
  41. package/languages/javascript.js +215 -27
  42. package/languages/python.js +113 -47
  43. package/languages/rust.js +89 -26
  44. package/languages/utils.js +35 -7
  45. package/mcp/server.js +69 -30
  46. package/package.json +4 -1
package/cli/index.js CHANGED
@@ -221,7 +221,7 @@ function parseFlags(tokens) {
221
221
  addParam: getValueFlag('--add-param'),
222
222
  removeParam: getValueFlag('--remove-param'),
223
223
  renameTo: getValueFlag('--rename-to'),
224
- defaultValue: getValueFlag('--default'),
224
+ defaultValue: getValueFlag('--default-value') ?? getValueFlag('--default'),
225
225
  base: getValueFlag('--base'),
226
226
  staged: tokens.includes('--staged') || undefined,
227
227
  deep: tokens.includes('--deep') || undefined,
@@ -234,6 +234,9 @@ function parseFlags(tokens) {
234
234
  diverse: tokens.includes('--diverse') || undefined,
235
235
  git: tokens.includes('--git') || undefined,
236
236
  className: getValueFlag('--class-name'),
237
+ // Explicit line pin (fix #249: our own disambiguation notes advertise
238
+ // line= but no surface accepted it).
239
+ line: parseInt(getValueFlag('--line') || '0', 10) || undefined,
237
240
  limit: parseInt(getValueFlag('--limit') || '0') || undefined,
238
241
  limitRaw: getValueFlag('--limit'),
239
242
  maxFiles: parseInt(getValueFlag('--max-files') || '0') || undefined,
@@ -286,11 +289,11 @@ const knownFlags = new Set([
286
289
  '--no-cache', '--clear-cache', '--include-tests', '--exclude-tests',
287
290
  '--include-exported', '--include-decorated', '--expand', '--interactive', '-i', '--all', '--include-methods', '--no-include-methods', '--include-uncertain', '--expand-unverified', '--detailed', '--calls-only',
288
291
  '--file', '--context', '--exclude', '--not', '--in',
289
- '--depth', '--direction', '--add-param', '--remove-param', '--rename-to',
292
+ '--depth', '--direction', '--add-param', '--remove-param', '--rename-to', '--default-value',
290
293
  '--default', '--top', '--no-follow-symlinks',
291
294
  '--base', '--staged', '--stack',
292
295
  '--regex', '--no-regex', '--functions', '--hot', '--diverse', '--git',
293
- '--max-lines', '--class-name', '--limit', '--max-files',
296
+ '--max-lines', '--class-name', '--line', '--limit', '--max-files',
294
297
  '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
295
298
  '--hide-confidence', '--no-confidence', '--min-confidence', '--unreachable-only',
296
299
  '--framework', '--workers', '--deep', '--compact',
@@ -344,8 +347,8 @@ try {
344
347
  // Value flags that consume the next token (space form: --flag value)
345
348
  const VALUE_FLAGS = new Set([
346
349
  '--file', '--depth', '--top', '--context', '--direction',
347
- '--add-param', '--remove-param', '--rename-to', '--default',
348
- '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name',
350
+ '--add-param', '--remove-param', '--rename-to', '--default', '--default-value',
351
+ '--base', '--exclude', '--not', '--in', '--max-lines', '--class-name', '--line',
349
352
  '--type', '--param', '--receiver', '--returns', '--decorator',
350
353
  '--limit', '--max-files', '--min-confidence', '--stack', '--framework',
351
354
  '--workers', '--method', '--prefix'
@@ -398,30 +401,40 @@ function printOutput(result, jsonFn, textFn) {
398
401
  }
399
402
 
400
403
  /**
401
- * Print inline 3-line code previews for each callee (--expand support).
402
- * Used by context in project, interactive, and glob modes.
404
+ * Print inline 3-line code previews for context items (--expand support).
405
+ * Used by context in project, interactive, and glob modes. Previews both
406
+ * directions with attribution headers (fix #252: callees only — the flag
407
+ * was a silent no-op for caller-only contexts, and the unlabeled preview
408
+ * rendered detached from the item it expanded).
403
409
  */
404
410
  function printInlineExpand(ctx, root) {
405
- if (!root || !ctx || !ctx.callees) return;
406
- for (const c of ctx.callees) {
407
- if (c.relativePath && c.startLine) {
408
- try {
409
- const filePath = path.join(root, c.relativePath);
410
- const content = fs.readFileSync(filePath, 'utf-8');
411
- const codeLines = content.split('\n');
412
- const endLine = c.endLine || c.startLine + 5;
413
- const previewLines = Math.min(3, endLine - c.startLine + 1);
414
- for (let i = 0; i < previewLines && c.startLine - 1 + i < codeLines.length; i++) {
415
- console.log(` │ ${codeLines[c.startLine - 1 + i]}`);
416
- }
417
- if (endLine - c.startLine + 1 > 3) {
418
- console.log(` ... (${endLine - c.startLine - 2} more lines)`);
419
- }
420
- } catch (e) {
421
- // Skip on error
411
+ if (!root || !ctx) return;
412
+ const preview = (c, label) => {
413
+ // Caller entries locate the enclosing FUNCTION via callerStartLine;
414
+ // callee entries are definitions with startLine.
415
+ const startLine = label === 'caller' ? c.callerStartLine : c.startLine;
416
+ const endLine = label === 'caller'
417
+ ? (c.callerEndLine || (startLine ? startLine + 5 : null))
418
+ : (c.endLine || (startLine ? startLine + 5 : null));
419
+ if (!c.relativePath || !startLine) return;
420
+ try {
421
+ const filePath = path.join(root, c.relativePath);
422
+ const content = fs.readFileSync(filePath, 'utf-8');
423
+ const codeLines = content.split('\n');
424
+ console.log(` ${label}: ${c.name || c.callerName || '(anonymous)'} ${c.relativePath}:${startLine}`);
425
+ const previewLines = Math.min(3, endLine - startLine + 1);
426
+ for (let i = 0; i < previewLines && startLine - 1 + i < codeLines.length; i++) {
427
+ console.log(` │ ${codeLines[startLine - 1 + i]}`);
428
+ }
429
+ if (endLine - startLine + 1 > 3) {
430
+ console.log(` │ ... (${endLine - startLine - 2} more lines)`);
422
431
  }
432
+ } catch (e) {
433
+ // Skip on error
423
434
  }
424
- }
435
+ };
436
+ for (const c of ctx.callers || []) preview(c, 'caller');
437
+ for (const c of ctx.callees || []) preview(c, 'callee');
425
438
  }
426
439
 
427
440
  // ============================================================================
@@ -460,21 +473,74 @@ function main() {
460
473
  target = positionalArgs[0];
461
474
  command = positionalArgs[1] || 'toc';
462
475
  arg = positionalArgs[2];
476
+ // lines takes `<file> <range>` as two positionals (fix #252 —
477
+ // the extra token was silently dropped and the command then
478
+ // demanded a --file the user had plainly given).
479
+ if (command === 'lines' && positionalArgs.length > 3) {
480
+ arg = positionalArgs.slice(2).join(' ');
481
+ }
463
482
  }
464
483
 
465
- // Determine mode: single file, glob pattern, or project
466
- if (target === '.' || (fs.existsSync(target) && fs.statSync(target).isDirectory())) {
467
- // Project mode
468
- runProjectCommand(target, command, arg);
469
- } else if (target.includes('*') || target.includes('{')) {
470
- // Glob pattern mode
471
- runGlobCommand(target, command, arg);
472
- } else if (fs.existsSync(target)) {
473
- // Single file mode
474
- runFileCommand(target, command, arg);
475
- } else {
476
- console.error(`Error: "${target}" not found`);
477
- process.exit(1);
484
+ // Determine mode: single file, glob pattern, or project.
485
+ // CommandError is the fail() control-flow signal — project mode catches
486
+ // it internally, but file/glob mode errors used to escape main() and
487
+ // dump a raw stack trace after the message (fix #249).
488
+ try {
489
+ if (target === '.' || (fs.existsSync(target) && fs.statSync(target).isDirectory())) {
490
+ // Project mode
491
+ runProjectCommand(target, command, arg);
492
+ } else if (target.includes('*') || target.includes('{')) {
493
+ // Glob pattern mode
494
+ runGlobCommand(target, command, arg);
495
+ } else if (fs.existsSync(target)) {
496
+ // Single file mode
497
+ runFileCommand(target, command, arg);
498
+ } else {
499
+ console.error(`Error: "${target}" not found`);
500
+ process.exit(1);
501
+ }
502
+ } catch (e) {
503
+ if (!(e instanceof CommandError)) {
504
+ console.error(`Error: ${e.message}`);
505
+ }
506
+ process.exitCode = 1;
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Parse the lines command's target forms (fix #252: `lines main.ts 1-10`
512
+ * silently dropped the second positional and demanded a --file that was
513
+ * plainly given): `<range>` (+ --file), `<file> <range>`, `<file>:<range>`.
514
+ */
515
+ function parseLinesTarget(arg, fileFlag) {
516
+ let file = fileFlag;
517
+ let range = arg;
518
+ const parts = String(arg || '').trim().split(/\s+/);
519
+ if (parts.length === 2 && /^\d+(-\d+)?$/.test(parts[1])) {
520
+ file = parts[0];
521
+ range = parts[1];
522
+ } else if (!/^\d+(-\d+)?$/.test(range)) {
523
+ const m = String(range).match(/^(.+):(\d+(?:-\d+)?)$/);
524
+ if (m) { file = m[1]; range = m[2]; }
525
+ }
526
+ return { file, range };
527
+ }
528
+
529
+ /**
530
+ * Tiered-output contract notes: unverified callers are always shown for
531
+ * these commands, so the legacy reveal flags are implied no-ops. Shared by
532
+ * one-shot and interactive mode (fix #250 — interactive printed nothing).
533
+ */
534
+ function printTieredNoOpNotes(canonical, flags, print) {
535
+ if (!['about', 'context', 'impact', 'trace', 'blast', 'reverseTrace', 'affectedTests', 'verify', 'smart'].includes(canonical)) return;
536
+ if (flags.includeUncertain) {
537
+ print(`Note: --include-uncertain has no effect on '${toCliName(canonical)}' — unverified candidates are always shown (tiered).`);
538
+ }
539
+ if (['impact', 'verify', 'blast', 'reverseTrace', 'affectedTests'].includes(canonical) && flags.includeMethods) {
540
+ print(`Note: --include-methods has no effect on '${toCliName(canonical)}' — method calls are always tiered by receiver evidence.`);
541
+ }
542
+ if (['about', 'context', 'smart'].includes(canonical) && flags.includeMethods) {
543
+ print(`Note: --include-methods on '${toCliName(canonical)}' affects only method-callee display for standalone-function targets — caller tiers are always evidence-based, and method targets analyze method calls by default.`);
478
544
  }
479
545
  }
480
546
 
@@ -546,7 +612,7 @@ function runFileCommand(filePath, command, arg) {
546
612
  case 'toc':
547
613
  printOutput(result, output.formatTocJson, r => output.formatToc(r, {
548
614
  detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
549
- uncertainHint: 'use --include-uncertain to include all'
615
+ uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
550
616
  }));
551
617
  break;
552
618
  case 'find':
@@ -660,16 +726,8 @@ function runProjectCommand(rootDir, command, arg) {
660
726
  console.error(`Warning: ${flagToCli(key)} has no effect on '${toCliName(canonical)}'.`);
661
727
  }
662
728
  }
663
- // Tiered-output contract: unverified callers are always shown for
664
- // these commands, so the legacy reveal flags are implied no-ops.
665
- if (['about', 'context', 'impact', 'trace', 'blast', 'reverseTrace', 'affectedTests'].includes(canonical)) {
666
- if (flags.includeUncertain) {
667
- console.error(`Note: --include-uncertain is implied for '${toCliName(canonical)}' — unverified candidates are always shown (tiered).`);
668
- }
669
- if (['about', 'context', 'impact'].includes(canonical) && flags.includeMethods) {
670
- console.error(`Note: --include-methods is implied for '${toCliName(canonical)}' — method calls are tiered by receiver evidence.`);
671
- }
672
- }
729
+ // Tiered-output contract notes (shared with interactive mode).
730
+ printTieredNoOpNotes(canonical, flags, (m) => console.error(m));
673
731
  }
674
732
 
675
733
  switch (canonical) {
@@ -681,7 +739,7 @@ function runProjectCommand(rootDir, command, arg) {
681
739
  if (note) console.error(note);
682
740
  printOutput(result, output.formatTocJson, r => output.formatToc(r, {
683
741
  detailedHint: 'Add --detailed to list all functions, or "ucn . about <name>" for full details on a symbol',
684
- uncertainHint: 'use --include-uncertain to include all'
742
+ uncertainHint: 'run "ucn . about <name>" for tiered detail on a specific symbol'
685
743
  }));
686
744
  break;
687
745
  }
@@ -735,9 +793,7 @@ function runProjectCommand(rootDir, command, arg) {
735
793
  console.log(output.formatContextJson(ctx));
736
794
  } else {
737
795
  const { text, expandable } = output.formatContext(ctx, {
738
- methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
739
796
  expandHint: 'Use "expand <N>" or --expand to see code for items',
740
- uncertainHint: 'use --include-uncertain to include all',
741
797
  showConfidence: flags.showConfidence !== false,
742
798
  compact: !!flags.compact,
743
799
  });
@@ -757,10 +813,12 @@ function runProjectCommand(rootDir, command, arg) {
757
813
 
758
814
  case 'expand': {
759
815
  requireArg(arg, 'Usage: ucn . expand <N>\nFirst run "ucn . context <name>" to get numbered items');
760
- const expandNum = parseInt(arg);
761
- if (isNaN(expandNum)) {
816
+ // Whole-token integers only (fix #248: parseInt truncation made
817
+ // `expand 1abc` and `expand 2.5` silently expand items 1 and 2).
818
+ if (!/^\d+$/.test(String(arg).trim())) {
762
819
  fail(`Invalid item number: "${arg}"`);
763
820
  }
821
+ const expandNum = parseInt(arg, 10);
764
822
  const cached = loadExpandableItems(index.root);
765
823
  const items = cached?.items || [];
766
824
  const match = items.find(i => i.num === expandNum);
@@ -798,7 +856,7 @@ function runProjectCommand(rootDir, command, arg) {
798
856
  const { ok, result, error, note } = execute(index, 'smart', { name: arg, ...flags });
799
857
  if (!ok) fail(error);
800
858
  printOutput(result, output.formatSmartJson, r => output.formatSmart(r, {
801
- uncertainHint: 'use --include-uncertain to include all'
859
+ uncertainHint: 'unverified callees are listed below with reasons'
802
860
  }));
803
861
  if (note) console.error(note);
804
862
  break;
@@ -878,7 +936,7 @@ function runProjectCommand(rootDir, command, arg) {
878
936
 
879
937
  case 'brief': {
880
938
  requireArg(arg, 'Usage: ucn . brief <name>');
881
- const { ok, result, error } = execute(index, 'brief', { name: arg, file: flags.file, className: flags.className, git: flags.git });
939
+ const { ok, result, error } = execute(index, 'brief', { name: arg, file: flags.file, className: flags.className, line: flags.line, git: flags.git });
882
940
  if (!ok) fail(error);
883
941
  printOutput(result, output.formatBriefJson, output.formatBrief);
884
942
  break;
@@ -894,6 +952,14 @@ function runProjectCommand(rootDir, command, arg) {
894
952
  break;
895
953
  }
896
954
 
955
+ case 'orient': {
956
+ const topVal = flags.topRaw != null ? flags.topRaw : (flags.top || undefined);
957
+ const { ok, result, error } = execute(index, 'orient', { top: topVal });
958
+ if (!ok) fail(error);
959
+ printOutput(result, output.formatOrientJson, output.formatOrient);
960
+ break;
961
+ }
962
+
897
963
  case 'check': {
898
964
  const { ok, result, error } = execute(index, 'check', {
899
965
  base: flags.base, staged: flags.staged,
@@ -908,7 +974,7 @@ function runProjectCommand(rootDir, command, arg) {
908
974
 
909
975
  case 'fn': {
910
976
  requireArg(arg, 'Usage: ucn . fn <name>');
911
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all, className: flags.className });
977
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: flags.file, all: flags.all, className: flags.className, line: flags.line });
912
978
  if (!ok) fail(error);
913
979
  if (note) console.error(note);
914
980
  printOutput(result, output.formatFnResultJson, output.formatFnResult);
@@ -917,7 +983,7 @@ function runProjectCommand(rootDir, command, arg) {
917
983
 
918
984
  case 'class': {
919
985
  requireArg(arg, 'Usage: ucn . class <name>');
920
- const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines });
986
+ const { ok, result, error, note } = execute(index, 'class', { name: arg, file: flags.file, all: flags.all, maxLines: flags.maxLines, line: flags.line });
921
987
  if (!ok) fail(error);
922
988
  if (note) console.error(note);
923
989
  printOutput(result, output.formatClassResultJson, output.formatClassResult);
@@ -925,8 +991,9 @@ function runProjectCommand(rootDir, command, arg) {
925
991
  }
926
992
 
927
993
  case 'lines': {
928
- requireArg(arg, 'Usage: ucn . lines <range> --file <path>');
929
- const { ok, result, error, note } = execute(index, 'lines', { file: flags.file, range: arg });
994
+ requireArg(arg, 'Usage: ucn . lines <range> --file <path> (or: lines <file> <range>)');
995
+ const linesTarget = parseLinesTarget(arg, flags.file);
996
+ const { ok, result, error, note } = execute(index, 'lines', { file: linesTarget.file, range: linesTarget.range });
930
997
  if (!ok) fail(error);
931
998
  if (note) console.error(note);
932
999
  printOutput(result, output.formatLinesJson, r => output.formatLines(r));
@@ -1052,7 +1119,7 @@ function runProjectCommand(rootDir, command, arg) {
1052
1119
  r => output.formatDeadcode(r, {
1053
1120
  top: flags.top,
1054
1121
  decoratedHint: !flags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
1055
- exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
1122
+ exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
1056
1123
  externalContractHint: !flags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
1057
1124
  })
1058
1125
  );
@@ -1329,8 +1396,6 @@ function runGlobCommand(pattern, command, arg) {
1329
1396
  console.log(output.formatContextJson(result));
1330
1397
  } else {
1331
1398
  const { text } = output.formatContext(result, {
1332
- methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1333
- uncertainHint: 'use --include-uncertain to include all',
1334
1399
  expandHint: 'Use --expand to see inline callee previews',
1335
1400
  showConfidence: flags.showConfidence !== false,
1336
1401
  compact: !!flags.compact,
@@ -1506,7 +1571,7 @@ FILE DEPENDENCIES
1506
1571
  ═══════════════════════════════════════════════════════════════════════════════
1507
1572
  REFACTORING HELPERS
1508
1573
  ═══════════════════════════════════════════════════════════════════════════════
1509
- plan <name> Preview refactoring (--add-param, --remove-param, --rename-to)
1574
+ plan <name> Preview refactoring (--add-param, --remove-param, --rename-to, --default-value)
1510
1575
  verify <name> Check all call sites match signature
1511
1576
  diff-impact What changed in git diff and who calls it (--base, --staged)
1512
1577
  check Pre-commit summary: diff-impact + verify + affected-tests in one shot
@@ -1523,6 +1588,7 @@ OTHER
1523
1588
  typedef <name> Find type definitions
1524
1589
  stats Project statistics (--functions for per-function line counts, --hot for top callers)
1525
1590
  doctor Project trust report (counts, blind spots, parse failures, verdict; --deep for resolution coverage)
1591
+ orient One-screen repo orientation: size, top dirs, hot functions, entry points, trust verdict (--top=N)
1526
1592
  stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
1527
1593
  audit-async Find calls in async functions that are likely missing await (JS/TS/Python)
1528
1594
 
@@ -1545,11 +1611,11 @@ Common Flags:
1545
1611
  --include-tests Include test files in usage counts (about) and results (find, usages, deadcode)
1546
1612
  --exclude-tests Exclude test files (entrypoints — tests are included by default)
1547
1613
  --class-name=X Scope to specific class (e.g., --class-name=Repository)
1548
- --include-methods Include method calls (obj.fn) in trace/blast/smart/verify analysis
1549
- (implied for about/context/impact — method calls are tiered by evidence)
1550
- --include-uncertain Include ambiguous/uncertain matches in smart/verify
1551
- (implied for about/context/impact/trace/blast/reverse-trace/affected-tests
1552
- unverified candidates always shown, tiered)
1614
+ --include-methods Include method-call (obj.fn) callee expansion in trace/smart
1615
+ (no effect on caller-direction commands — about/context/impact/verify/
1616
+ blast/reverse-trace/affected-tests always tier method calls by evidence)
1617
+ --include-uncertain No effect on tiered commands unverified candidates are always
1618
+ shown in their own section with reasons
1553
1619
  --expand-unverified Follow unverified caller edges in blast/reverse-trace trees
1554
1620
  (downstream nodes marked as unverified chains — possible, not confirmed, impact)
1555
1621
  --hide-confidence Hide confidence scores (shown by default in about, context)
@@ -1587,6 +1653,7 @@ Common Flags:
1587
1653
  -v, --version Print the UCN version and exit
1588
1654
 
1589
1655
  Quick Start:
1656
+ ucn orient # First look at a new repo
1590
1657
  ucn toc # See project structure
1591
1658
  ucn about handleRequest # Understand a function
1592
1659
  ucn impact handleRequest # Before modifying
@@ -1604,9 +1671,26 @@ function runInteractive(rootDir) {
1604
1671
 
1605
1672
  console.log('Building index...');
1606
1673
  const index = new ProjectIndex(rootDir);
1607
- index.build(null, { quiet: true, workers: flags.workers });
1674
+ // Same cache discipline as one-shot mode (fix #250: the REPL fully
1675
+ // re-parsed every session and never consumed cache-persisted state —
1676
+ // the divergence mechanism behind the relocation P1).
1677
+ let iCacheFresh = false;
1678
+ if (flags.cache && !flags.clearCache) {
1679
+ const loaded = index.loadCache();
1680
+ iCacheFresh = loaded && !index.isCacheStale();
1681
+ if (!iCacheFresh && loaded) {
1682
+ index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
1683
+ } else if (!iCacheFresh) {
1684
+ index.build(null, { quiet: true, workers: flags.workers });
1685
+ }
1686
+ if (!iCacheFresh) {
1687
+ try { index.saveCache(); } catch (_) { /* best-effort */ }
1688
+ }
1689
+ } else {
1690
+ index.build(null, { quiet: true, workers: flags.workers });
1691
+ }
1608
1692
  const iExpandCache = new ExpandCache({ maxSize: 20 });
1609
- console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
1693
+ console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1610
1694
  console.log('Type commands (e.g., "find parseFile", "about main", "toc")');
1611
1695
  console.log('Type "help" for commands, "quit" to exit\n');
1612
1696
 
@@ -1662,7 +1746,7 @@ Commands:
1662
1746
  typedef <name> Find type definitions
1663
1747
  deadcode Find unused functions/classes
1664
1748
  verify <name> Check call sites match signature
1665
- plan <name> Preview refactoring (--add-param=, --remove-param=, --rename-to=)
1749
+ plan <name> Preview refactoring (--add-param=, --remove-param=, --rename-to=, --default-value=)
1666
1750
  stacktrace <text> Parse a stack trace
1667
1751
  api Show public symbols
1668
1752
  diff-impact What changed and who's affected
@@ -1681,7 +1765,7 @@ Flags can be added per-command: context myFunc --include-methods
1681
1765
  index.build(null, { quiet: true, forceRebuild: true, workers: flags.workers });
1682
1766
  // Clear expand cache — stale line ranges after rebuild
1683
1767
  if (iExpandCache) iExpandCache.clearForRoot(index.root);
1684
- console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} symbols`);
1768
+ console.log(`Index ready: ${index.files.size} files, ${index.symbols.size} unique symbol names`);
1685
1769
  rl.prompt();
1686
1770
  return;
1687
1771
  }
@@ -1690,7 +1774,7 @@ Flags can be added per-command: context myFunc --include-methods
1690
1774
  const tokens = input.split(/\s+/);
1691
1775
  const command = tokens[0];
1692
1776
  // Flags that take a space-separated value (--flag value)
1693
- const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--framework', '--method', '--prefix']);
1777
+ const valueFlagNames = new Set(['--file', '--in', '--base', '--add-param', '--remove-param', '--rename-to', '--default', '--depth', '--top', '--context', '--max-lines', '--direction', '--exclude', '--not', '--stack', '--type', '--param', '--receiver', '--returns', '--decorator', '--limit', '--max-files', '--min-confidence', '--class-name', '--line', '--framework', '--method', '--prefix']);
1694
1778
  const flagTokens = [];
1695
1779
  const argTokens = [];
1696
1780
  const skipNext = new Set();
@@ -1708,7 +1792,23 @@ Flags can be added per-command: context myFunc --include-methods
1708
1792
  }
1709
1793
  }
1710
1794
  const arg = argTokens.join(' ');
1795
+
1796
+ // Unknown flags error instead of folding their VALUE into the
1797
+ // symbol name (fix #250: `about AddTask --bogus 5` searched for
1798
+ // "AddTask 5"). Same vocabulary as one-shot mode.
1799
+ const unknown = flagTokens.filter(t =>
1800
+ t.startsWith('--') && !knownFlags.has(t.split('=')[0]));
1801
+ if (unknown.length > 0) {
1802
+ console.log(`Unknown flag(s): ${unknown.join(', ')}`);
1803
+ rl.prompt();
1804
+ return;
1805
+ }
1711
1806
  const iflags = parseFlags(flagTokens);
1807
+ // parseFlags never extracts --json (it is a global-argv flag), so
1808
+ // check the raw tokens.
1809
+ if (flagTokens.includes('--json')) {
1810
+ console.log('Note: interactive mode prints text output — run `ucn . <command> --json` one-shot for JSON.');
1811
+ }
1712
1812
 
1713
1813
  try {
1714
1814
  // Validate numeric flags (--top, --limit, etc) — same rules as
@@ -1747,9 +1847,9 @@ Flags can be added per-command: context myFunc --include-methods
1747
1847
 
1748
1848
  const INTERACTIVE_DISPATCH = {
1749
1849
  // ── Understanding Code ───────────────────────────────────────────
1750
- about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false, git: !!f.git }) },
1751
- smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'use --include-uncertain to include all' }) },
1752
- impact: { params: 'name', format: (r) => output.formatImpact(r) },
1850
+ about: { params: 'name', format: (r, _a, f, idx) => output.formatAbout(r, { expand: f.expand, root: idx.root, showAll: f.all, depth: f.depth, showConfidence: f.showConfidence !== false, compact: !!f.compact, git: !!f.git }) },
1851
+ smart: { params: 'name', format: (r) => output.formatSmart(r, { uncertainHint: 'unverified callees are listed below with reasons' }) },
1852
+ impact: { params: 'name', format: (r, _a, f) => output.formatImpact(r, { compact: !!f.compact }) },
1753
1853
  blast: { params: 'name', format: (r) => output.formatBlast(r) },
1754
1854
  trace: { params: 'name', format: (r) => output.formatTrace(r) },
1755
1855
  reverseTrace: { params: 'name', format: (r) => output.formatReverseTrace(r) },
@@ -1759,8 +1859,8 @@ const INTERACTIVE_DISPATCH = {
1759
1859
 
1760
1860
  // ── Finding Code ─────────────────────────────────────────────────
1761
1861
  find: { params: 'name', format: (r, a, f) => output.formatFindDetailed(r, a, { depth: f.depth, top: f.top, all: f.all }) },
1762
- usages: { params: 'name', format: (r, a) => output.formatUsages(r, a) },
1763
- 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' }) },
1862
+ usages: { params: 'name', format: (r, a, f) => output.formatUsages(r, a, { compact: !!f.compact }) },
1863
+ toc: { params: 'flags', format: (r) => output.formatToc(r, { detailedHint: 'Add --detailed to list all functions, or "about <name>" for full details on a symbol', uncertainHint: 'run "about <name>" for tiered detail on a specific symbol' }) },
1764
1864
  tests: { params: 'name', format: (r, a) => output.formatTests(r, a) },
1765
1865
  affectedTests: { params: 'name', format: (r, _a, f) => output.formatAffectedTests(r, { all: f.all }) },
1766
1866
  typedef: { params: 'name', format: (r, a) => output.formatTypedef(r, a) },
@@ -1781,9 +1881,10 @@ const INTERACTIVE_DISPATCH = {
1781
1881
  endpoints: { params: (a, f) => ({ file: f.file, exclude: f.exclude, limit: f.limit, framework: f.framework, bridge: f.bridge, serverOnly: f.serverOnly, clientOnly: f.clientOnly, unmatched: f.unmatched, method: f.method, prefix: f.prefix, hideUncertain: f.hideUncertain }), format: (r) => output.formatEndpoints(r, { bridge: r._bridge, unmatched: r._unmatched }) },
1782
1882
 
1783
1883
  // ── Other ────────────────────────────────────────────────────────
1784
- api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file || '.') },
1884
+ api: { params: (a, f) => ({ file: a || f.file, limit: f.limit }), format: (r, a, f) => output.formatApi(r, a || f.file) },
1785
1885
  stacktrace: { params: (a, f) => ({ stack: f.stack || a }), format: (r) => output.formatStackTrace(r) },
1786
1886
  doctor: { params: (a, f) => ({ file: f.file, in: f.in, limit: f.limit, deep: f.deep }), format: (r) => output.formatDoctor(r) },
1887
+ orient: { params: (a, f) => ({ top: f.topRaw != null ? f.topRaw : (f.top || undefined) }), format: (r) => output.formatOrient(r) },
1787
1888
  // MED-2: stats handler in execute.js rejects top<=0; without explicit
1788
1889
  // coercion, parseFlags's `top: 0` default would surface as
1789
1890
  // "Invalid --top value" on bare `stats`. Mirror the project-mode top
@@ -1822,6 +1923,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1822
1923
  console.log(`Warning: ${flagToCli(key)} has no effect on '${command}'.`);
1823
1924
  }
1824
1925
  }
1926
+ // Tiered-output contract notes (fix #250 — one-shot mode printed
1927
+ // these; interactive silently accepted the flags).
1928
+ printTieredNoOpNotes(command, iflags, (m) => console.log(m));
1825
1929
  }
1826
1930
 
1827
1931
  // ── Commands with unique behavior (not data-driven) ──────────────
@@ -1829,25 +1933,26 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1829
1933
 
1830
1934
  case 'fn': {
1831
1935
  if (!arg) { console.log('Usage: fn <name>[,name2,...] [--file=<pattern>] [--class-name=<class>]'); return; }
1832
- const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all, className: iflags.className });
1936
+ const { ok, result, error, note } = execute(index, 'fn', { name: arg, file: iflags.file, all: iflags.all, className: iflags.className, line: iflags.line });
1833
1937
  if (!ok) { console.log(error); return; }
1834
- if (note) console.log(note);
1835
1938
  console.log(output.formatFnResult(result));
1939
+ if (note) console.log(note);
1836
1940
  break;
1837
1941
  }
1838
1942
 
1839
1943
  case 'class': {
1840
1944
  if (!arg) { console.log('Usage: class <name> [--file=<pattern>]'); return; }
1841
- const { ok, result, error, note } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines });
1945
+ const { ok, result, error, note } = execute(index, 'class', { name: arg, file: iflags.file, all: iflags.all, maxLines: iflags.maxLines, line: iflags.line });
1842
1946
  if (!ok) { console.log(error); return; }
1843
- if (note) console.log(note);
1844
1947
  console.log(output.formatClassResult(result));
1948
+ if (note) console.log(note);
1845
1949
  break;
1846
1950
  }
1847
1951
 
1848
1952
  case 'lines': {
1849
- if (!arg) { console.log('Usage: lines <range> --file=<file>'); return; }
1850
- const { ok, result, error } = execute(index, 'lines', { file: iflags.file, range: arg });
1953
+ if (!arg) { console.log('Usage: lines <range> --file=<file> (or: lines <file> <range>)'); return; }
1954
+ const iLinesTarget = parseLinesTarget(arg, iflags.file);
1955
+ const { ok, result, error } = execute(index, 'lines', { file: iLinesTarget.file, range: iLinesTarget.range });
1851
1956
  if (!ok) { console.log(error); return; }
1852
1957
  console.log(output.formatLines(result));
1853
1958
  break;
@@ -1858,11 +1963,12 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1858
1963
  console.log('Usage: expand <number>');
1859
1964
  return;
1860
1965
  }
1861
- const expandNum = parseInt(arg, 10);
1862
- if (isNaN(expandNum)) {
1966
+ // Whole-token integers only (fix #248: parseInt truncation).
1967
+ if (!/^\d+$/.test(String(arg).trim())) {
1863
1968
  console.log(`Invalid item number: "${arg}"`);
1864
1969
  return;
1865
1970
  }
1971
+ const expandNum = parseInt(arg, 10);
1866
1972
  let match, itemCount, symbolName;
1867
1973
  if (cache) {
1868
1974
  const lookup = cache.lookup(index.root, expandNum);
@@ -1887,10 +1993,9 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1887
1993
  const { ok, result, error, note } = execute(index, 'context', { name: arg, ...iflags });
1888
1994
  if (!ok) { console.log(error); return; }
1889
1995
  const { text, expandable } = output.formatContext(result, {
1890
- methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1891
1996
  expandHint: 'Use "expand <N>" to see code for item N',
1892
- uncertainHint: 'use --include-uncertain to include all',
1893
1997
  showConfidence: iflags.showConfidence !== false,
1998
+ compact: !!iflags.compact,
1894
1999
  });
1895
2000
  console.log(text);
1896
2001
  if (iflags.expand) {
@@ -1908,25 +2013,25 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1908
2013
  case 'deadcode': {
1909
2014
  const { ok, result, error, note } = execute(index, 'deadcode', iflags);
1910
2015
  if (!ok) { console.log(error); return; }
1911
- if (note) console.log(note);
1912
2016
  console.log(output.formatDeadcode(result, {
1913
2017
  top: iflags.top,
1914
2018
  decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
1915
- exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
2019
+ exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.` : undefined,
1916
2020
  externalContractHint: !iflags.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.` : undefined
1917
2021
  }));
2022
+ if (note) console.log(note);
1918
2023
  break;
1919
2024
  }
1920
2025
 
1921
2026
  case 'search': {
1922
2027
  const { ok, result, error, structural, note } = execute(index, 'search', { term: arg, ...iflags });
1923
2028
  if (!ok) { console.log(error); return; }
1924
- if (note) console.log(note);
1925
2029
  if (structural) {
1926
2030
  console.log(output.formatStructuralSearch(result));
1927
2031
  } else {
1928
2032
  console.log(output.formatSearch(result, arg));
1929
2033
  }
2034
+ if (note) console.log(note);
1930
2035
  break;
1931
2036
  }
1932
2037
 
@@ -1940,8 +2045,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1940
2045
  const params = buildInteractiveParams(entry.params, arg, iflags);
1941
2046
  const { ok, result, error, note } = execute(index, command, params);
1942
2047
  if (!ok) { console.log(error); return; }
1943
- if (note) console.log(note);
1944
2048
  console.log(entry.format(result, arg, iflags, index));
2049
+ if (note) console.log(note);
1945
2050
  }
1946
2051
  }
1947
2052
  }
package/core/account.js CHANGED
@@ -72,7 +72,9 @@ function computeGroundSet(index, name) {
72
72
  continue; // deleted since indexing; not part of the universe anymore
73
73
  }
74
74
  if (!content.includes(name)) continue;
75
- const lines = content.split('\n');
75
+ // Shared read-only lines array split once per file per operation,
76
+ // reused across the symbols of a multi-symbol command (diff-impact).
77
+ const lines = index._getFileLines(filePath);
76
78
  const matched = [];
77
79
  for (let i = 0; i < lines.length; i++) {
78
80
  if (wordRe.test(lines[i])) matched.push(i + 1);