ucn 3.7.47 → 3.8.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/core/execute.js CHANGED
@@ -241,6 +241,50 @@ const HANDLERS = {
241
241
  return { ok: true, result };
242
242
  },
243
243
 
244
+ blast: (index, p) => {
245
+ const err = requireName(p.name);
246
+ if (err) return { ok: false, error: err };
247
+ applyClassMethodSyntax(p);
248
+ const fileErr = checkFilePatternMatch(index, p.file);
249
+ if (fileErr) return { ok: false, error: fileErr };
250
+ const classErr = validateClassName(index, p.name, p.className);
251
+ if (classErr) return { ok: false, error: classErr };
252
+ const depthVal = num(p.depth, undefined);
253
+ const result = index.blast(p.name, {
254
+ depth: depthVal ?? 3,
255
+ file: p.file,
256
+ className: p.className,
257
+ all: p.all || depthVal !== undefined,
258
+ exclude: toExcludeArray(p.exclude),
259
+ includeMethods: p.includeMethods,
260
+ includeUncertain: p.includeUncertain || false,
261
+ });
262
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
263
+ return { ok: true, result };
264
+ },
265
+
266
+ reverseTrace: (index, p) => {
267
+ const err = requireName(p.name);
268
+ if (err) return { ok: false, error: err };
269
+ applyClassMethodSyntax(p);
270
+ const fileErr = checkFilePatternMatch(index, p.file);
271
+ if (fileErr) return { ok: false, error: fileErr };
272
+ const classErr = validateClassName(index, p.name, p.className);
273
+ if (classErr) return { ok: false, error: classErr };
274
+ const depthVal = num(p.depth, undefined);
275
+ const result = index.reverseTrace(p.name, {
276
+ depth: depthVal ?? 5,
277
+ file: p.file,
278
+ className: p.className,
279
+ all: p.all || depthVal !== undefined,
280
+ exclude: toExcludeArray(p.exclude),
281
+ includeMethods: p.includeMethods,
282
+ includeUncertain: p.includeUncertain || false,
283
+ });
284
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
285
+ return { ok: true, result };
286
+ },
287
+
244
288
  smart: (index, p) => {
245
289
  const err = requireName(p.name);
246
290
  if (err) return { ok: false, error: err };
@@ -424,6 +468,30 @@ const HANDLERS = {
424
468
  },
425
469
 
426
470
  search: (index, p) => {
471
+ // Detect structural search mode: any of these flags triggers index-based search
472
+ const isStructural = p.type || p.param || p.receiver || p.returns || p.decorator || p.exported || p.unused;
473
+ if (isStructural) {
474
+ const exclude = applyTestExclusions(p.exclude, p.includeTests);
475
+ const topVal = num(p.top, undefined) || num(p.limit, undefined);
476
+ const result = index.structuralSearch({
477
+ term: p.term || p.name,
478
+ type: p.type,
479
+ param: p.param,
480
+ receiver: p.receiver,
481
+ returns: p.returns,
482
+ decorator: p.decorator,
483
+ exported: p.exported || false,
484
+ unused: p.unused || false,
485
+ caseSensitive: p.caseSensitive || false,
486
+ exclude,
487
+ in: p.in,
488
+ file: p.file,
489
+ top: topVal || 50,
490
+ });
491
+ if (result.meta.error) return { ok: false, error: result.meta.error };
492
+ return { ok: true, result, structural: true };
493
+ }
494
+
427
495
  const err = requireTerm(p.term);
428
496
  if (err) return { ok: false, error: err };
429
497
  const testsExcluded = !p.includeTests;
@@ -455,6 +523,27 @@ const HANDLERS = {
455
523
  return { ok: true, result };
456
524
  },
457
525
 
526
+ affectedTests: (index, p) => {
527
+ const err = requireName(p.name);
528
+ if (err) return { ok: false, error: err };
529
+ applyClassMethodSyntax(p);
530
+ const fileErr = checkFilePatternMatch(index, p.file);
531
+ if (fileErr) return { ok: false, error: fileErr };
532
+ const classErr = validateClassName(index, p.name, p.className);
533
+ if (classErr) return { ok: false, error: classErr };
534
+ const depthVal = num(p.depth, undefined);
535
+ const result = index.affectedTests(p.name, {
536
+ depth: depthVal ?? 3,
537
+ file: p.file,
538
+ className: p.className,
539
+ exclude: toExcludeArray(p.exclude),
540
+ includeMethods: p.includeMethods,
541
+ includeUncertain: p.includeUncertain || false,
542
+ });
543
+ if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
544
+ return { ok: true, result };
545
+ },
546
+
458
547
  deadcode: (index, p) => {
459
548
  const fileErr = checkFilePatternMatch(index, p.file);
460
549
  if (fileErr) return { ok: false, error: fileErr };
@@ -704,6 +793,14 @@ const HANDLERS = {
704
793
  return { ok: true, result };
705
794
  },
706
795
 
796
+ circularDeps: (index, p) => {
797
+ const result = index.circularDeps({
798
+ file: p.file,
799
+ exclude: toExcludeArray(p.exclude),
800
+ });
801
+ return { ok: true, result };
802
+ },
803
+
707
804
  // ── Refactoring ─────────────────────────────────────────────────────
708
805
 
709
806
  verify: (index, p) => {
package/core/output.js CHANGED
@@ -854,6 +854,289 @@ function formatTraceJson(trace) {
854
854
  return JSON.stringify(trace, null, 2);
855
855
  }
856
856
 
857
+ /**
858
+ * Format blast command output - text
859
+ * Shows transitive blast radius (callers of callers)
860
+ */
861
+ function formatBlast(blast, options = {}) {
862
+ if (!blast) {
863
+ return 'Function not found.';
864
+ }
865
+
866
+ const lines = [];
867
+
868
+ // Header
869
+ lines.push(`Blast radius for ${blast.root}`);
870
+ lines.push('═'.repeat(60));
871
+ lines.push(`${blast.file}:${blast.line}`);
872
+ lines.push(`Depth: ${blast.maxDepth}`);
873
+
874
+ if (blast.warnings && blast.warnings.length > 0) {
875
+ for (const w of blast.warnings) {
876
+ lines.push(`Note: ${w.message}`);
877
+ }
878
+ }
879
+
880
+ lines.push('');
881
+
882
+ // Render tree (same structure as trace but showing callers)
883
+ let hasTruncation = false;
884
+ const renderNode = (node, prefix = '', isLast = true) => {
885
+ const connector = isLast ? '└── ' : '├── ';
886
+ const extension = isLast ? ' ' : '│ ';
887
+
888
+ let label = node.name;
889
+ if (node.file) {
890
+ label += ` (${node.file}:${node.line})`;
891
+ }
892
+ if (node.callSites && node.callSites > 1) {
893
+ label += ` ${node.callSites}x`;
894
+ }
895
+ if (node.alreadyShown) {
896
+ label += ' (see above)';
897
+ }
898
+
899
+ lines.push(prefix + connector + label);
900
+
901
+ if (node.children && !node.alreadyShown) {
902
+ const hasMore = node.truncatedChildren > 0;
903
+ for (let i = 0; i < node.children.length; i++) {
904
+ const isChildLast = !hasMore && i === node.children.length - 1;
905
+ renderNode(node.children[i], prefix + extension, isChildLast);
906
+ }
907
+ if (hasMore) {
908
+ hasTruncation = true;
909
+ lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
910
+ }
911
+ }
912
+ };
913
+
914
+ // Root node
915
+ lines.push(blast.root);
916
+ if (blast.tree && blast.tree.children) {
917
+ const rootHasMore = blast.tree.truncatedChildren > 0;
918
+ for (let i = 0; i < blast.tree.children.length; i++) {
919
+ const isLast = !rootHasMore && i === blast.tree.children.length - 1;
920
+ renderNode(blast.tree.children[i], '', isLast);
921
+ }
922
+ if (rootHasMore) {
923
+ hasTruncation = true;
924
+ lines.push(`└── ... and ${blast.tree.truncatedChildren} more callers`);
925
+ }
926
+ }
927
+
928
+ // Summary
929
+ if (blast.summary) {
930
+ lines.push('');
931
+ const { totalAffected, totalFiles } = blast.summary;
932
+ if (totalAffected > 0) {
933
+ lines.push(`Summary: 1 function changed → ${totalAffected} function${totalAffected !== 1 ? 's' : ''} affected across ${totalFiles} file${totalFiles !== 1 ? 's' : ''}`);
934
+ } else {
935
+ lines.push('Summary: No callers found — this function is a root/entry point.');
936
+ }
937
+ }
938
+
939
+ if (hasTruncation) {
940
+ const allHint = options.allHint || 'Use --all to show all.';
941
+ lines.push(`\nSome results truncated. ${allHint}`);
942
+ }
943
+
944
+ if (blast.includeMethods === false) {
945
+ lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
946
+ }
947
+
948
+ return lines.join('\n');
949
+ }
950
+
951
+ /**
952
+ * Format blast command output - JSON
953
+ */
954
+ function formatBlastJson(blast) {
955
+ if (!blast) {
956
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
957
+ }
958
+ return JSON.stringify(blast, null, 2);
959
+ }
960
+
961
+ /**
962
+ * Format reverse-trace command output - text
963
+ * Shows upward call chain to entry points
964
+ */
965
+ function formatReverseTrace(result, options = {}) {
966
+ if (!result) {
967
+ return 'Function not found.';
968
+ }
969
+
970
+ const lines = [];
971
+
972
+ // Header
973
+ lines.push(`Reverse trace for ${result.root}`);
974
+ lines.push('═'.repeat(60));
975
+ lines.push(`${result.file}:${result.line}`);
976
+ lines.push(`Depth: ${result.maxDepth}`);
977
+
978
+ if (result.warnings && result.warnings.length > 0) {
979
+ for (const w of result.warnings) {
980
+ lines.push(`Note: ${w.message}`);
981
+ }
982
+ }
983
+
984
+ lines.push('');
985
+
986
+ // Render tree
987
+ let hasTruncation = false;
988
+ const renderNode = (node, prefix = '', isLast = true) => {
989
+ const connector = isLast ? '└── ' : '├── ';
990
+ const extension = isLast ? ' ' : '│ ';
991
+
992
+ let label = node.name;
993
+ if (node.file) {
994
+ label += ` (${node.file}:${node.line})`;
995
+ }
996
+ if (node.callSites && node.callSites > 1) {
997
+ label += ` ${node.callSites}x`;
998
+ }
999
+ if (node.entryPoint) {
1000
+ label += ' ★ entry point';
1001
+ }
1002
+ if (node.alreadyShown) {
1003
+ label += ' (see above)';
1004
+ }
1005
+
1006
+ lines.push(prefix + connector + label);
1007
+
1008
+ if (node.children && !node.alreadyShown) {
1009
+ const hasMore = node.truncatedChildren > 0;
1010
+ for (let i = 0; i < node.children.length; i++) {
1011
+ const isChildLast = !hasMore && i === node.children.length - 1;
1012
+ renderNode(node.children[i], prefix + extension, isChildLast);
1013
+ }
1014
+ if (hasMore) {
1015
+ hasTruncation = true;
1016
+ lines.push(prefix + extension + `└── ... and ${node.truncatedChildren} more callers`);
1017
+ }
1018
+ }
1019
+ };
1020
+
1021
+ // Root node
1022
+ let rootLabel = result.root;
1023
+ if (result.tree && result.tree.entryPoint) {
1024
+ rootLabel += ' ★ entry point (no callers)';
1025
+ }
1026
+ lines.push(rootLabel);
1027
+ if (result.tree && result.tree.children) {
1028
+ const rootHasMore = result.tree.truncatedChildren > 0;
1029
+ for (let i = 0; i < result.tree.children.length; i++) {
1030
+ const isLast = !rootHasMore && i === result.tree.children.length - 1;
1031
+ renderNode(result.tree.children[i], '', isLast);
1032
+ }
1033
+ if (rootHasMore) {
1034
+ hasTruncation = true;
1035
+ lines.push(`└── ... and ${result.tree.truncatedChildren} more callers`);
1036
+ }
1037
+ }
1038
+
1039
+ // Entry points summary
1040
+ if (result.entryPoints && result.entryPoints.length > 0) {
1041
+ lines.push('');
1042
+ lines.push(`Entry points (${result.entryPoints.length}):`);
1043
+ for (const ep of result.entryPoints) {
1044
+ lines.push(` ★ ${ep.name} (${ep.file}:${ep.line})`);
1045
+ }
1046
+ }
1047
+
1048
+ // Summary
1049
+ if (result.summary) {
1050
+ lines.push('');
1051
+ const { totalEntryPoints, totalFunctions } = result.summary;
1052
+ if (totalFunctions > 0) {
1053
+ lines.push(`Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root} through ${totalFunctions} intermediate function${totalFunctions !== 1 ? 's' : ''}`);
1054
+ } else {
1055
+ lines.push('Summary: No callers found — this function is itself an entry point.');
1056
+ }
1057
+ }
1058
+
1059
+ if (hasTruncation) {
1060
+ const allHint = options.allHint || 'Use --all to show all.';
1061
+ lines.push(`\nSome results truncated. ${allHint}`);
1062
+ }
1063
+
1064
+ if (result.includeMethods === false) {
1065
+ lines.push('\nNote: obj.method() calls excluded. Use --include-methods to include them.');
1066
+ }
1067
+
1068
+ return lines.join('\n');
1069
+ }
1070
+
1071
+ /**
1072
+ * Format reverse-trace command output - JSON
1073
+ */
1074
+ function formatReverseTraceJson(result) {
1075
+ if (!result) {
1076
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1077
+ }
1078
+ return JSON.stringify(result, null, 2);
1079
+ }
1080
+
1081
+ /**
1082
+ * Format affected-tests command output - text
1083
+ */
1084
+ function formatAffectedTests(result) {
1085
+ if (!result) return 'Function not found.';
1086
+
1087
+ const lines = [];
1088
+ const { summary } = result;
1089
+
1090
+ lines.push(`affected-tests: ${result.root}`);
1091
+ lines.push('═'.repeat(60));
1092
+ lines.push(`${result.file}:${result.line}`);
1093
+ lines.push(`1 function changed → ${summary.totalAffected} functions affected (depth ${result.depth})`);
1094
+ lines.push('');
1095
+
1096
+ if (result.testFiles.length === 0) {
1097
+ lines.push('No test files found for any affected function.');
1098
+ } else {
1099
+ lines.push(`Test files to run (${summary.totalTestFiles}):`);
1100
+ lines.push('');
1101
+ for (const tf of result.testFiles) {
1102
+ lines.push(` ${tf.file} (covers: ${tf.coveredFunctions.join(', ')})`);
1103
+ // Show up to 5 key matches per file
1104
+ const keyMatches = tf.matches
1105
+ .filter(m => m.matchType === 'call' || m.matchType === 'test-case')
1106
+ .slice(0, 5);
1107
+ for (const m of keyMatches) {
1108
+ lines.push(` L${m.line}: ${m.content} [${m.matchType}]`);
1109
+ }
1110
+ }
1111
+ }
1112
+
1113
+ if (result.uncovered.length > 0) {
1114
+ lines.push('');
1115
+ lines.push(`Uncovered (${result.uncovered.length}): ${result.uncovered.join(', ')}`);
1116
+ lines.push(' ⚠ These affected functions have no test references');
1117
+ }
1118
+
1119
+ lines.push('');
1120
+ const pct = summary.totalAffected > 0
1121
+ ? Math.round(summary.coveredFunctions / summary.totalAffected * 100)
1122
+ : 0;
1123
+ lines.push(`Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.coveredFunctions}/${summary.totalAffected} functions covered (${pct}%)`);
1124
+
1125
+ if (result.warnings?.length > 0) {
1126
+ lines.push('');
1127
+ for (const w of result.warnings) lines.push(`Note: ${w.message}`);
1128
+ }
1129
+
1130
+ return lines.join('\n');
1131
+ }
1132
+
1133
+ function formatAffectedTestsJson(result) {
1134
+ if (!result) {
1135
+ return JSON.stringify({ found: false, error: 'Function not found' }, null, 2);
1136
+ }
1137
+ return JSON.stringify(result, null, 2);
1138
+ }
1139
+
857
1140
  /**
858
1141
  * Format related command output - text
859
1142
  */
@@ -2008,6 +2291,43 @@ function formatGraph(graph, options = {}) {
2008
2291
  return lines.join('\n');
2009
2292
  }
2010
2293
 
2294
+ function formatCircularDeps(result) {
2295
+ if (!result) return 'No results.';
2296
+ const lines = [];
2297
+
2298
+ lines.push('Circular dependencies');
2299
+ lines.push('═'.repeat(60));
2300
+
2301
+ if (result.fileFilter) {
2302
+ lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
2303
+ }
2304
+
2305
+ if (result.cycles.length === 0) {
2306
+ lines.push('');
2307
+ lines.push('No circular dependencies found.');
2308
+ lines.push(`Scanned ${result.totalFiles} files.`);
2309
+ return lines.join('\n');
2310
+ }
2311
+
2312
+ for (let i = 0; i < result.cycles.length; i++) {
2313
+ const cycle = result.cycles[i];
2314
+ lines.push('');
2315
+ lines.push(`Cycle ${i + 1} (${cycle.length} files):`);
2316
+ lines.push(` ${cycle.files.join(' → ')} → ${cycle.files[0]}`);
2317
+ }
2318
+
2319
+ lines.push('');
2320
+ const { totalCycles, filesInCycles } = result.summary;
2321
+ lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''} out of ${result.totalFiles} total.`);
2322
+
2323
+ return lines.join('\n');
2324
+ }
2325
+
2326
+ function formatCircularDepsJson(result) {
2327
+ if (!result) return JSON.stringify({ error: 'No results' }, null, 2);
2328
+ return JSON.stringify(result, null, 2);
2329
+ }
2330
+
2011
2331
  /**
2012
2332
  * Detect common double-escaping patterns in regex search terms.
2013
2333
  * When MCP/JSON transport is involved, agents often write \\. when they mean \. (literal dot).
@@ -2077,6 +2397,67 @@ function formatSearch(results, term) {
2077
2397
  return lines.join('\n');
2078
2398
  }
2079
2399
 
2400
+ /**
2401
+ * Format structural search results (index-based queries)
2402
+ */
2403
+ function formatStructuralSearch(result) {
2404
+ const { results, meta } = result;
2405
+ const lines = [];
2406
+
2407
+ // Build query description
2408
+ const parts = [];
2409
+ if (meta.query.type) parts.push(`type=${meta.query.type}`);
2410
+ if (meta.query.term) parts.push(`name="${meta.query.term}"`);
2411
+ if (meta.query.param) parts.push(`param="${meta.query.param}"`);
2412
+ if (meta.query.receiver) parts.push(`receiver="${meta.query.receiver}"`);
2413
+ if (meta.query.returns) parts.push(`returns="${meta.query.returns}"`);
2414
+ if (meta.query.decorator) parts.push(`decorator="${meta.query.decorator}"`);
2415
+ if (meta.query.exported) parts.push('exported');
2416
+ if (meta.query.unused) parts.push('unused');
2417
+ const queryStr = parts.join(', ');
2418
+
2419
+ lines.push(`Structural search: ${queryStr}`);
2420
+ lines.push('═'.repeat(60));
2421
+
2422
+ if (results.length === 0) {
2423
+ lines.push('No matches found.');
2424
+ return lines.join('\n');
2425
+ }
2426
+
2427
+ lines.push(`Found ${meta.totalMatched} match${meta.totalMatched === 1 ? '' : 'es'}${meta.shown < meta.totalMatched ? ` (showing ${meta.shown})` : ''}:`);
2428
+ lines.push('');
2429
+
2430
+ // Group by file
2431
+ let currentFile = null;
2432
+ for (const r of results) {
2433
+ if (r.file !== currentFile) {
2434
+ currentFile = r.file;
2435
+ lines.push(`${r.file}`);
2436
+ }
2437
+
2438
+ if (r.kind === 'call') {
2439
+ lines.push(` ${r.line}: ${r.name}()${r.isMethod ? ' [method]' : ''}`);
2440
+ } else {
2441
+ let sig = ` ${r.line}: ${r.kind} ${r.name}`;
2442
+ if (r.params) sig += `(${r.params})`;
2443
+ if (r.returnType) sig += ` → ${r.returnType}`;
2444
+ if (r.className) sig += ` [${r.className}]`;
2445
+ if (r.decorators) sig += ` @${r.decorators.join(', @')}`;
2446
+ lines.push(sig);
2447
+ }
2448
+ }
2449
+
2450
+ if (meta.shown < meta.totalMatched) {
2451
+ lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more.`);
2452
+ }
2453
+
2454
+ return lines.join('\n');
2455
+ }
2456
+
2457
+ function formatStructuralSearchJson(result) {
2458
+ return JSON.stringify(result, null, 2);
2459
+ }
2460
+
2080
2461
  /**
2081
2462
  * Format file-exports command output
2082
2463
  */
@@ -2746,6 +3127,18 @@ module.exports = {
2746
3127
  formatTrace,
2747
3128
  formatTraceJson,
2748
3129
 
3130
+ // Blast command
3131
+ formatBlast,
3132
+ formatBlastJson,
3133
+
3134
+ // Reverse trace command
3135
+ formatReverseTrace,
3136
+ formatReverseTraceJson,
3137
+
3138
+ // Affected tests command
3139
+ formatAffectedTests,
3140
+ formatAffectedTestsJson,
3141
+
2749
3142
  // Related command
2750
3143
  formatRelated,
2751
3144
  formatRelatedJson,
@@ -2767,7 +3160,11 @@ module.exports = {
2767
3160
  formatFn,
2768
3161
  formatClass,
2769
3162
  formatGraph,
3163
+ formatCircularDeps,
3164
+ formatCircularDepsJson,
2770
3165
  formatSearch,
3166
+ formatStructuralSearch,
3167
+ formatStructuralSearchJson,
2771
3168
  detectDoubleEscaping,
2772
3169
  formatFileExports,
2773
3170
  formatStats,