ucn 4.0.1 → 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.
- package/.claude/skills/ucn/SKILL.md +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +334 -316
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +67 -33
- package/core/output/check.js +4 -1
- package/core/output/doctor.js +13 -3
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +220 -84
- package/core/search.js +270 -55
- package/core/shared.js +285 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +72 -31
- 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
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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: '
|
|
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
|
|
664
|
-
|
|
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: '
|
|
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
|
-
|
|
761
|
-
|
|
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: '
|
|
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
|
|
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 (
|
|
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
|
|
1549
|
-
(
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
-
|
|
1862
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
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);
|