ucn 5.5.3 → 5.6.1

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 (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
package/core/execute.js CHANGED
@@ -15,7 +15,7 @@ const path = require('path');
15
15
  const { addTestExclusions, pickBestDefinition, parseSymbolHandle, looksLikeHandle } = require('./shared');
16
16
  const { cleanHtmlScriptTags, detectLanguage } = require('./parser');
17
17
  const { renderExpandItem } = require('./expand-cache');
18
- const { CANONICAL_COMMANDS } = require('./registry');
18
+ const { CANONICAL_COMMANDS, FLAG_APPLICABILITY, commandMode, paramDefault, paramTableError, toCliName } = require('./registry');
19
19
  const { sourceRangeTarget } = require('./public-command');
20
20
  const { isAccessorDefinition } = require('./accessors');
21
21
  const { describeError } = require('./errors');
@@ -24,11 +24,11 @@ const { describeError } = require('./errors');
24
24
  // HELPERS
25
25
  // ============================================================================
26
26
 
27
+ // A state, said once and short: what the absence never means is in the
28
+ // tool description and the usage guide.
27
29
  const NO_STATIC_TEST_LINK_NOTE =
28
- 'No confirmed static test link was found. This is not runtime coverage evidence: ' +
29
- 'subprocess/black-box tests, reflection, generated code, and external harnesses can still ' +
30
- 'exercise the target. Inspect the project test runner and search for the defining module/file ' +
31
- 'before concluding it is untested.';
30
+ 'No static test link found; this is not runtime coverage evidence ' +
31
+ '(tests that reach the target by subprocess, reflection or generated code are not seen).';
32
32
 
33
33
  function requireName(name) {
34
34
  if (!name || (typeof name === 'string' && !name.trim())) {
@@ -99,17 +99,20 @@ function symbolNotFound(index, name, label = 'Symbol') {
99
99
  } else {
100
100
  message = `${label} "${query}" not found. Use find with a shorter name, or search for a literal occurrence.`;
101
101
  }
102
+ // Unanalyzed source is searched for this name; it is mentioned only
103
+ // when the name occurs in it, or when part of the project could not be
104
+ // searched at all.
105
+ const universeTools = require('./source-universe');
102
106
  const unsupported = require('./account').scanUnsupportedFiles(index, query);
103
107
  if (unsupported.lines > 0) {
104
- const languages = Object.keys(unsupported.languages).join(', ');
108
+ const labels = universeTools.matchLabels(unsupported);
105
109
  message += ` ${unsupported.lines} literal match(es) exist in ` +
106
- `${unsupported.fileCount} unsupported-language file(s)` +
107
- `${languages ? ` (${languages})` : ''}; this is not a semantic zero. ` +
108
- 'Use grep/ripgrep plus a language-native analyzer.';
109
- } else if (index.unsupportedFiles?.length > 0) {
110
- message += ` UCN skipped ${index.unsupportedFiles.length} unsupported source ` +
111
- `file(s), so this is not a repository-wide semantic zero; verify with grep/ripgrep.`;
110
+ `${unsupported.fileCount} unanalyzed file(s)` +
111
+ `${labels ? ` (${labels})` : ''}; this is not a semantic zero. ` +
112
+ universeTools.matchAdvice(unsupported);
112
113
  }
114
+ const unsearched = universeTools.unsearchedNote(index);
115
+ if (unsearched) message += ` ${unsearched}`;
113
116
  return message;
114
117
  }
115
118
 
@@ -213,7 +216,7 @@ function buildCallerOptions(p) {
213
216
  includeUncertain: p.includeUncertain || false,
214
217
  includeTests: p.includeTests,
215
218
  exclude: toExcludeArray(p.exclude),
216
- minConfidence: num(p.minConfidence, 0),
219
+ minConfidence: num(p.minConfidence, paramDefault('minConfidence')),
217
220
  };
218
221
  }
219
222
 
@@ -294,9 +297,12 @@ function num(val, fallback) {
294
297
  return isNaN(n) ? fallback : n;
295
298
  }
296
299
 
297
- /** Shared listing bound; shell listings remain uncapped unless requested. */
298
- function listingLimit(p) {
299
- return num(p.limit, undefined) || (p.lines || p.all ? undefined : 500);
300
+ /**
301
+ * The listing bound of `command`: an explicit limit, else the command's
302
+ * default limit. Shell listings and `all` have no default bound.
303
+ */
304
+ function listingLimit(p, command) {
305
+ return num(p.limit, undefined) || (p.lines || p.all ? undefined : paramDefault('limit', command));
300
306
  }
301
307
 
302
308
  /**
@@ -421,31 +427,17 @@ function readAndExtract(match) {
421
427
  return cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
422
428
  }
423
429
 
424
- const SHOW_SECTIONS = new Set([
425
- 'summary', 'callers', 'callees', 'source', 'dependencies',
426
- 'tests', 'types', 'example', 'related',
427
- ]);
428
- const REPO_SECTIONS = new Set(['summary', 'files', 'stats', 'health']);
429
-
430
- /** Parse a comma-separated/array section selector with validation. */
431
- function parseSections(value, defaults, allowed, command) {
432
- const supplied = value != null && value !== '';
430
+ /**
431
+ * The sections a `show` or `repo` request selects: the ones it names, else
432
+ * the command's default for its mode. The parameter table has already
433
+ * refused a name outside the command's set.
434
+ */
435
+ function selectedSections(command, p) {
436
+ const supplied = p.sections != null && p.sections !== '';
433
437
  const raw = !supplied
434
- ? defaults
435
- : (Array.isArray(value) ? value : String(value).split(','));
436
- const sections = [...new Set(raw.map(s => String(s).trim().toLowerCase()).filter(Boolean))];
437
- if (supplied && sections.length === 0) {
438
- return {
439
- error: `No valid ${command} sections were provided. Available: ${[...allowed].join(', ')}.`,
440
- };
441
- }
442
- const invalid = sections.filter(s => !allowed.has(s));
443
- if (invalid.length > 0) {
444
- return {
445
- error: `Unknown ${command} section(s): ${invalid.join(', ')}. Available: ${[...allowed].join(', ')}.`,
446
- };
447
- }
448
- return { sections };
438
+ ? paramDefault('sections', command, commandMode(command, p))
439
+ : (Array.isArray(p.sections) ? p.sections : String(p.sections).split(','));
440
+ return [...new Set(raw.map(s => String(s).trim().toLowerCase()).filter(Boolean))];
449
441
  }
450
442
 
451
443
  function addMode(result, mode) {
@@ -471,14 +463,17 @@ const PUBLIC_COMMAND_SET = new Set(CANONICAL_COMMANDS);
471
463
  * glob, file, and programmatic callers receive the same answer.
472
464
  */
473
465
  function validatePublicParams(command, p) {
474
- if (p.raw && command !== 'source') return '--raw is supported only by source.';
475
- if (p.lines && !['find', 'usages', 'search', 'show', 'impact'].includes(command)) {
476
- return '--lines is supported by find, usages, search, show, and impact.';
477
- }
478
- if (p.lines && command === 'show' && p.sections) {
479
- const parsed = parseSections(p.sections, ['callers'], new Set(['callers', 'callees']), 'show --lines');
480
- if (parsed.error) return parsed.error;
466
+ // The shell modes change the shape of the whole answer: on a command
467
+ // that has none they are refused, not ignored.
468
+ for (const shape of ['raw', 'lines']) {
469
+ if (!p[shape] || FLAG_APPLICABILITY[command].includes(shape)) continue;
470
+ const takers = CANONICAL_COMMANDS.filter(name => FLAG_APPLICABILITY[name].includes(shape)).map(toCliName);
471
+ return takers.length === 1
472
+ ? `--${shape} is supported only by ${takers[0]}.`
473
+ : `--${shape} is supported by ${takers.slice(0, -1).join(', ')}, and ${takers.at(-1)}.`;
481
474
  }
475
+ const tableError = paramTableError(command, p);
476
+ if (tableError) return tableError;
482
477
  const hasName = typeof p.name === 'string' && p.name.trim() !== '';
483
478
  const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
484
479
  if (command === 'impact' && hasName && gitScope) {
@@ -504,14 +499,14 @@ function validatePublicParams(command, p) {
504
499
  return 'search receiver filtering requires type=call (or omit type to select call mode).';
505
500
  }
506
501
  if (command === 'trace' && p.to === 'entrypoints' &&
507
- (p.direction || 'callees') !== 'callers') {
502
+ (p.direction || paramDefault('direction', 'trace')) !== 'callers') {
508
503
  return 'trace --to=entrypoints requires --direction=callers.';
509
504
  }
510
505
  if (command === 'deps' && p.cycles &&
511
506
  (p.file || p.direction || p.depth != null || p.detailed || p.all)) {
512
507
  return 'deps cycles mode cannot be combined with file, direction, depth, detailed, or all.';
513
508
  }
514
- if (command === 'tests' && Number(p.depth || 0) > 0 && p.callsOnly) {
509
+ if (command === 'tests' && Number(p.depth || paramDefault('depth', 'tests')) > 0 && p.callsOnly) {
515
510
  return 'tests callsOnly applies to direct mode and cannot be combined with depth>0.';
516
511
  }
517
512
  if (command === 'entrypoints' && p.includeTests === true && p.excludeTests === true) {
@@ -523,8 +518,9 @@ function validatePublicParams(command, p) {
523
518
  if (command === 'plan') {
524
519
  const operations = [p.addParam, p.removeParam, p.renameTo]
525
520
  .filter(value => value != null && String(value).trim() !== '');
521
+ if (p.delete) operations.push('delete');
526
522
  if (operations.length !== 1) {
527
- return 'plan requires exactly one operation: addParam, removeParam, or renameTo.';
523
+ return 'plan requires exactly one operation: addParam, removeParam, renameTo, or delete.';
528
524
  }
529
525
  if (p.defaultValue != null && !p.addParam) {
530
526
  return 'plan defaultValue is valid only with addParam.';
@@ -587,13 +583,7 @@ const HANDLERS = {
587
583
  line: resolved.def.nameLine || resolved.def.startLine,
588
584
  ...(resolved.def.className && { className: resolved.def.className }),
589
585
  } : query;
590
- const parsed = parseSections(
591
- p.sections,
592
- p.lines ? ['callers'] : ['summary', 'callers', 'callees'],
593
- SHOW_SECTIONS,
594
- 'show',
595
- );
596
- if (parsed.error) return { ok: false, error: parsed.error };
586
+ const parsed = { sections: selectedSections('show', p) };
597
587
  if (p.withTypes && !parsed.sections.includes('types')) parsed.sections.push('types');
598
588
 
599
589
  const selected = new Set(parsed.sections);
@@ -729,10 +719,7 @@ const HANDLERS = {
729
719
  }
730
720
  const err = requireFile(p.file);
731
721
  if (err) return { ok: false, error: err };
732
- const direction = p.direction || 'both';
733
- if (!['imports', 'importers', 'both'].includes(direction)) {
734
- return { ok: false, error: 'deps direction must be imports, importers, or both.' };
735
- }
722
+ const direction = p.direction || paramDefault('direction', 'deps');
736
723
  const graphResponse = HANDLERS.graph(index, {
737
724
  file: p.file,
738
725
  direction,
@@ -764,9 +751,7 @@ const HANDLERS = {
764
751
  if (fileErr) return { ok: false, error: fileErr };
765
752
  const inErr = validateInFilter(index, p.in);
766
753
  if (inErr) return { ok: false, error: inErr };
767
- const defaults = p.deep ? ['summary', 'health'] : ['summary'];
768
- const parsed = parseSections(p.sections, defaults, REPO_SECTIONS, 'repo');
769
- if (parsed.error) return { ok: false, error: parsed.error };
754
+ const parsed = { sections: selectedSections('repo', p) };
770
755
  const selected = new Set(parsed.sections);
771
756
  const result = { sections: parsed.sections };
772
757
  const notes = [];
@@ -783,7 +768,7 @@ const HANDLERS = {
783
768
  ...p,
784
769
  // repo's public limit caps the file result set. Direct toc's
785
770
  // legacy symbol-list cap is intentionally not composed here.
786
- top: p.top || listingLimit(p),
771
+ top: p.top || listingLimit(p, 'repo'),
787
772
  limit: undefined,
788
773
  });
789
774
  if (!failure && selected.has('stats')) failure = collect('stats', 'stats', p);
@@ -880,6 +865,7 @@ const HANDLERS = {
880
865
  line: p.line,
881
866
  exclude: toExcludeArray(p.exclude),
882
867
  top: num(p.limit, undefined) || num(p.top, undefined),
868
+ all: !!p.all,
883
869
  unreachableOnly: !!p.unreachableOnly,
884
870
  // BUG-H3: pass through user-supplied flags. impact defaults to including
885
871
  // method calls because "what breaks if I change this" should include
@@ -907,7 +893,7 @@ const HANDLERS = {
907
893
  const depthVal = num(p.depth, undefined);
908
894
  const result = index.blast(p.name, {
909
895
  ...buildCallerOptions(p),
910
- depth: depthVal ?? 3,
896
+ depth: depthVal ?? paramDefault('depth', 'trace'),
911
897
  all: !!p.all,
912
898
  expandUnverified: !!p.expandUnverified,
913
899
  });
@@ -931,7 +917,7 @@ const HANDLERS = {
931
917
  const depthVal = num(p.depth, undefined);
932
918
  const result = index.reverseTrace(p.name, {
933
919
  ...buildCallerOptions(p),
934
- depth: depthVal ?? 5,
920
+ depth: depthVal ?? paramDefault('depth', 'trace', 'entrypoints'),
935
921
  all: !!p.all,
936
922
  expandUnverified: !!p.expandUnverified,
937
923
  });
@@ -962,13 +948,7 @@ const HANDLERS = {
962
948
  },
963
949
 
964
950
  trace: (index, p) => {
965
- const direction = p.direction || 'callees';
966
- if (!['callees', 'callers'].includes(direction)) {
967
- return { ok: false, error: 'trace direction must be callees or callers.' };
968
- }
969
- if (p.to && p.to !== 'entrypoints') {
970
- return { ok: false, error: 'trace --to currently accepts only entrypoints.' };
971
- }
951
+ const direction = p.direction || paramDefault('direction', 'trace');
972
952
  if (direction === 'callers') {
973
953
  const response = p.to === 'entrypoints'
974
954
  ? HANDLERS.reverseTrace(index, { ...p, direction: undefined, to: undefined })
@@ -991,7 +971,7 @@ const HANDLERS = {
991
971
  const depthVal = num(p.depth, undefined);
992
972
  const result = index.trace(p.name, {
993
973
  ...buildCallerOptions(p),
994
- depth: depthVal ?? 3,
974
+ depth: depthVal ?? paramDefault('depth', 'trace'),
995
975
  all: !!p.all,
996
976
  expandUnverified: !!p.expandUnverified,
997
977
  });
@@ -1128,10 +1108,11 @@ const HANDLERS = {
1128
1108
  const { check } = require('./check');
1129
1109
  try {
1130
1110
  const result = check(index, {
1131
- base: p.base || 'HEAD',
1111
+ base: p.base || paramDefault('base'),
1132
1112
  staged: !!p.staged,
1133
1113
  file: p.file,
1134
1114
  limit: num(p.limit, undefined),
1115
+ strict: !!p.strict,
1135
1116
  });
1136
1117
  addMode(result, 'diff');
1137
1118
  return { ok: true, result };
@@ -1145,21 +1126,13 @@ const HANDLERS = {
1145
1126
  find: (index, p) => {
1146
1127
  const inErr = validateInFilter(index, p.in);
1147
1128
  if (inErr) return { ok: false, error: inErr };
1148
- const validFindTypes = new Set([
1149
- 'type', 'function', 'class', 'variable', 'state', 'constant',
1150
- 'field', 'macro', 'method', 'constructor', 'interface', 'enum',
1151
- 'struct', 'trait', 'record', 'namespace',
1152
- ]);
1153
- if (p.type && !validFindTypes.has(p.type)) {
1154
- return { ok: false, error: `Invalid find type "${p.type}". Valid types: ${[...validFindTypes].join(', ')}.` };
1155
- }
1156
1129
  if (p.type === 'type') {
1157
1130
  const response = HANDLERS.typedef(index, { ...p });
1158
1131
  if (response.ok) {
1159
1132
  if (!p.withSource && Array.isArray(response.result)) {
1160
1133
  response.result = response.result.map(({ code, ...item }) => item);
1161
1134
  }
1162
- const limit = listingLimit(p);
1135
+ const limit = listingLimit(p, 'find');
1163
1136
  if (limit && limit > 0 && Array.isArray(response.result)) {
1164
1137
  const { items, total, limited } = applyLimit(response.result, limit);
1165
1138
  response.result = items;
@@ -1211,12 +1184,14 @@ const HANDLERS = {
1211
1184
  item.startLine === line || item.nameLine === line);
1212
1185
  }
1213
1186
  if (p.type) {
1214
- const kindGroups = {
1215
- function: new Set(['function', 'method', 'constructor', 'get', 'set']),
1216
- class: new Set(CLASS_KIND_TYPES),
1217
- variable: new Set(['state', 'constant', 'field']),
1218
- };
1219
- const kinds = kindGroups[p.type] || new Set([p.type]);
1187
+ // A Map: a kind may be named like an inherited object property
1188
+ // (`constructor`), which a plain object lookup would return.
1189
+ const kindGroups = new Map([
1190
+ ['function', new Set(['function', 'method', 'constructor', 'get', 'set'])],
1191
+ ['class', new Set(CLASS_KIND_TYPES)],
1192
+ ['variable', new Set(['state', 'constant', 'field'])],
1193
+ ]);
1194
+ const kinds = kindGroups.get(p.type) || new Set([p.type]);
1220
1195
  result = result.filter(item => kinds.has(item.type));
1221
1196
  }
1222
1197
  const fullFindCount = result.length;
@@ -1232,21 +1207,24 @@ const HANDLERS = {
1232
1207
  notes.push(`Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`);
1233
1208
  }
1234
1209
  let unsupportedMatches = null;
1235
- if (fullFindCount === 0 && index.unsupportedFiles?.length > 0) {
1236
- unsupportedMatches = require('./account').scanUnsupportedFiles(index, p.name);
1237
- const languages = Object.keys(unsupportedMatches.languages).join(', ');
1238
- if (unsupportedMatches.lines > 0) {
1239
- notes.push(`${unsupportedMatches.lines} literal match(es) exist in ` +
1240
- `${unsupportedMatches.fileCount} unsupported-language file(s)` +
1241
- `${languages ? ` (${languages})` : ''}; this is not a semantic zero. ` +
1242
- 'Use grep/ripgrep plus a language-native analyzer.');
1243
- } else {
1244
- notes.push(`UCN skipped ${index.unsupportedFiles.length} unsupported source ` +
1245
- 'file(s), so this is not a repository-wide semantic zero; verify with grep/ripgrep.');
1210
+ if (fullFindCount === 0) {
1211
+ const universeTools = require('./source-universe');
1212
+ if (universeTools.hasTextOnly(index)) {
1213
+ const matches = require('./account').scanUnsupportedFiles(index, p.name);
1214
+ if (matches.lines > 0) {
1215
+ unsupportedMatches = matches;
1216
+ const labels = universeTools.matchLabels(matches);
1217
+ notes.push(`${matches.lines} literal match(es) exist in ` +
1218
+ `${matches.fileCount} unanalyzed file(s)` +
1219
+ `${labels ? ` (${labels})` : ''}; this is not a semantic zero. ` +
1220
+ universeTools.matchAdvice(matches));
1221
+ }
1246
1222
  }
1223
+ const unsearched = universeTools.unsearchedNote(index);
1224
+ if (unsearched) notes.push(unsearched);
1247
1225
  }
1248
1226
  // Apply limit
1249
- const limit = listingLimit(p);
1227
+ const limit = listingLimit(p, 'find');
1250
1228
  if (!p.lines) {
1251
1229
  index._beginOp();
1252
1230
  try {
@@ -1305,7 +1283,7 @@ const HANDLERS = {
1305
1283
  const userExclude = toExcludeArray(p.exclude);
1306
1284
  const unfiltered = index.usages(p.name, {
1307
1285
  codeOnly: p.codeOnly || false,
1308
- context: num(p.context, 0),
1286
+ context: num(p.context, paramDefault('context')),
1309
1287
  className: p.className,
1310
1288
  // A stable handle pins which definition is meant; it must not
1311
1289
  // silently turn into a same-file-only usage scan. An explicit
@@ -1363,15 +1341,20 @@ const HANDLERS = {
1363
1341
  const accountTools = require('./account');
1364
1342
  const unsupportedMatches = accountTools.scanUnsupportedFiles(index, p.name);
1365
1343
  if (unsupportedMatches.lines > 0) {
1366
- const langs = Object.keys(unsupportedMatches.languages).join(', ');
1344
+ const langs = require('./source-universe').matchLabels(unsupportedMatches);
1367
1345
  notes.push(`${unsupportedMatches.lines} line(s) in ${unsupportedMatches.fileCount} ` +
1368
- `unsupported-language file(s)${langs ? ` (${langs})` : ''} also match — ` +
1346
+ `unanalyzed file(s)${langs ? ` (${langs})` : ''} also match — ` +
1369
1347
  'NOT analyzed by UCN; verify with grep/ripgrep.');
1370
1348
  Object.defineProperty(limited, 'unsupportedMatches', {
1371
1349
  value: unsupportedMatches,
1372
1350
  enumerable: false, writable: true, configurable: true,
1373
1351
  });
1374
1352
  }
1353
+ // An inventory is incomplete wherever a path could not be searched
1354
+ // (asked after the scan above: a file that fails to read while being
1355
+ // scanned becomes such a path).
1356
+ const unsearched = require('./source-universe').unsearchedNote(index);
1357
+ if (unsearched) notes.push(unsearched);
1375
1358
  const failed = accountTools.scanFailedFiles(index, p.name);
1376
1359
  if (failed.unparsed.lines > 0) {
1377
1360
  notes.push(`${failed.unparsed.lines} matching line(s) in ${failed.unparsed.fileCount} unparsed file(s) were not classified: ${failed.unparsed.files.join(', ')}.`);
@@ -1455,7 +1438,7 @@ const HANDLERS = {
1455
1438
  const fileErr = checkFilePatternMatch(index, p.file);
1456
1439
  if (fileErr) return { ok: false, error: fileErr };
1457
1440
  // Detect structural search mode: any of these flags triggers index-based search
1458
- const isStructural = p.type || p.param || p.receiver || p.returns || p.decorator || p.exported || p.unused;
1441
+ const isStructural = commandMode('search', p) === 'structural';
1459
1442
  if (isStructural) {
1460
1443
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
1461
1444
  const topVal = num(p.top, undefined) || num(p.limit, undefined);
@@ -1473,7 +1456,7 @@ const HANDLERS = {
1473
1456
  testExclude: p.includeTests ? undefined : ['test files'],
1474
1457
  in: p.in,
1475
1458
  file: p.file,
1476
- top: topVal || (p.lines ? undefined : 50),
1459
+ top: topVal || (p.lines ? undefined : paramDefault('limit', 'search', 'structural')),
1477
1460
  });
1478
1461
  if (result.meta.error) return { ok: false, error: result.meta.error };
1479
1462
  const unsupported = (!p.regex && (p.term || p.name))
@@ -1487,7 +1470,7 @@ const HANDLERS = {
1487
1470
  value: unsupported,
1488
1471
  enumerable: false, writable: true, configurable: true,
1489
1472
  });
1490
- note = combineNotes([note, `${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not structurally analyzed; verify with grep/ripgrep.`]);
1473
+ note = combineNotes([note, `${unsupported.lines} matching line(s) in ${unsupported.fileCount} unanalyzed file(s) were not structurally analyzed; verify with grep/ripgrep.`]);
1491
1474
  }
1492
1475
  return { ok: true, result, structural: true, note };
1493
1476
  }
@@ -1497,14 +1480,14 @@ const HANDLERS = {
1497
1480
  const testsExcluded = !p.includeTests;
1498
1481
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
1499
1482
  // JSON is intentionally structured rather than byte-truncated, so
1500
- // text search needs a row bound of its own. Keep the documented
1501
- // default at 500 and retain total/truncated metadata for controlled
1502
- // expansion with an explicit top/limit.
1483
+ // text search needs a row bound of its own: the default limit, with
1484
+ // total/truncated metadata for controlled expansion by an explicit
1485
+ // top/limit.
1503
1486
  const topVal = num(p.top, undefined) || num(p.limit, undefined);
1504
- const effectiveLimit = topVal || (p.lines ? undefined : 500);
1487
+ const effectiveLimit = topVal || (p.lines ? undefined : paramDefault('limit', 'search'));
1505
1488
  const result = index.search(p.term, {
1506
1489
  codeOnly: p.codeOnly || false,
1507
- context: num(p.context, 0),
1490
+ context: num(p.context, paramDefault('context')),
1508
1491
  caseSensitive: p.caseSensitive || false,
1509
1492
  exclude,
1510
1493
  in: p.in,
@@ -1521,8 +1504,10 @@ const HANDLERS = {
1521
1504
  value: unsupported,
1522
1505
  enumerable: false, writable: true, configurable: true,
1523
1506
  });
1524
- notes.push(`${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not searched; verify with grep/ripgrep.`);
1507
+ notes.push(`${unsupported.lines} matching line(s) in ${unsupported.fileCount} unanalyzed file(s) were not searched; verify with grep/ripgrep.`);
1525
1508
  }
1509
+ const unsearched = require('./source-universe').unsearchedNote(index);
1510
+ if (unsearched) notes.push(unsearched);
1526
1511
  }
1527
1512
  const tNote = truncationNote(index);
1528
1513
  if (tNote) notes.push(tNote);
@@ -1530,7 +1515,7 @@ const HANDLERS = {
1530
1515
  },
1531
1516
 
1532
1517
  tests: (index, p) => {
1533
- const depth = num(p.depth, 0);
1518
+ const depth = num(p.depth, paramDefault('depth', 'tests'));
1534
1519
  if (depth > 0) {
1535
1520
  const response = HANDLERS.affectedTests(index, { ...p, depth });
1536
1521
  if (response.ok) addMode(response.result, 'affected');
@@ -1614,15 +1599,17 @@ const HANDLERS = {
1614
1599
  if (!testsTargetIsFile) {
1615
1600
  const unsupportedMatches = require('./account').scanUnsupportedFiles(index, p.name);
1616
1601
  if (unsupportedMatches.lines > 0) {
1617
- const langs = Object.keys(unsupportedMatches.languages).join(', ');
1602
+ const langs = require('./source-universe').matchLabels(unsupportedMatches);
1618
1603
  testNotes.push(`"${p.name}" also occurs on ${unsupportedMatches.lines} line(s) in ` +
1619
- `${unsupportedMatches.fileCount} unsupported-language file(s)${langs ? ` (${langs})` : ''} — ` +
1604
+ `${unsupportedMatches.fileCount} unanalyzed file(s)${langs ? ` (${langs})` : ''} — ` +
1620
1605
  'their test files are invisible to UCN; verify with grep/ripgrep.');
1621
1606
  Object.defineProperty(result, 'unsupportedMatches', {
1622
1607
  value: unsupportedMatches,
1623
1608
  enumerable: false, writable: true, configurable: true,
1624
1609
  });
1625
1610
  }
1611
+ const unsearched = require('./source-universe').unsearchedNote(index);
1612
+ if (unsearched) testNotes.push(unsearched);
1626
1613
  }
1627
1614
  return {
1628
1615
  ok: true,
@@ -1675,7 +1662,7 @@ const HANDLERS = {
1675
1662
  file: p.file,
1676
1663
  });
1677
1664
  // Apply limit to dead code results (result is an array with custom properties)
1678
- const limit = listingLimit(p);
1665
+ const limit = listingLimit(p, 'deadcode');
1679
1666
  let note;
1680
1667
  if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
1681
1668
  note = limitNote(limit, result.length);
@@ -1687,6 +1674,8 @@ const HANDLERS = {
1687
1674
  if (result.excludedRuntimeContract != null) sliced.excludedRuntimeContract = result.excludedRuntimeContract;
1688
1675
  if (result.excludedConstructors != null) sliced.excludedConstructors = result.excludedConstructors;
1689
1676
  if (result.excludedDynamicDispatch != null) sliced.excludedDynamicDispatch = result.excludedDynamicDispatch;
1677
+ if (result.excludedRequiredSlot != null) sliced.excludedRequiredSlot = result.excludedRequiredSlot;
1678
+ if (result.projectKind != null) sliced.projectKind = result.projectKind;
1690
1679
  if (result.pythonImplicitExportFiles != null) sliced.pythonImplicitExportFiles = result.pythonImplicitExportFiles;
1691
1680
  if (result.computedDispatch != null) sliced.computedDispatch = result.computedDispatch;
1692
1681
  if (result.reflection != null) sliced.reflection = result.reflection;
@@ -1780,21 +1769,12 @@ const HANDLERS = {
1780
1769
  endpoints: (index, p) => {
1781
1770
  const fileErr = checkFilePatternMatch(index, p.file);
1782
1771
  if (fileErr) return { ok: false, error: fileErr };
1783
- // Minor polish: validate --method against known HTTP verbs to catch
1784
- // typos that would otherwise silently filter out everything.
1785
- // 'ALL' / 'USE' are downstream route labels that can be queried explicitly.
1786
- const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'ALL', 'USE']);
1787
- let normMethod = null;
1788
- if (p.method != null && String(p.method).trim() !== '') {
1789
- normMethod = String(p.method).trim().toUpperCase();
1790
- if (!HTTP_METHODS.has(normMethod)) {
1791
- return {
1792
- ok: false,
1793
- error: `Invalid --method value: "${p.method}". Expected one of ${[...HTTP_METHODS].join(', ')}.`,
1794
- };
1795
- }
1796
- }
1797
- const { endpoints } = require('./bridge');
1772
+ // The method is one of the table's verbs in any letter case; a typo
1773
+ // is refused there instead of filtering everything out.
1774
+ const normMethod = p.method != null && String(p.method).trim() !== ''
1775
+ ? String(p.method).trim().toUpperCase()
1776
+ : null;
1777
+ const { endpoints, ENDPOINT_FRAMEWORKS } = require('./bridge');
1798
1778
  // HIGH-2: --unmatched implies --bridge (we need bridges to know what's
1799
1779
  // unmatched). Without bridges computed, we can't separate matched from
1800
1780
  // unmatched on either side.
@@ -1813,16 +1793,14 @@ const HANDLERS = {
1813
1793
  });
1814
1794
  if (p.framework != null && String(p.framework).trim() !== '') {
1815
1795
  const framework = String(p.framework).trim().toLowerCase();
1816
- const known = new Set([
1817
- 'actix', 'aspnet', 'aspnet-minimal', 'axios', 'axum',
1818
- 'dotnet-httpclient', 'express', 'fastapi', 'fetch', 'flask',
1819
- 'go-http', 'jax-rs', 'nestjs', 'nextjs', 'requests',
1820
- 'reqwest', 'spring', 'spring-client', 'unknown-python',
1821
- ]);
1822
- if (!known.has(framework)) {
1796
+ // Known: a label the endpoint detectors declare, or one a row of
1797
+ // this answer carries. A label the answer prints always filters.
1798
+ const present = [...result.routes, ...result.requests].map(row => row.framework).filter(Boolean);
1799
+ const known = [...new Set([...ENDPOINT_FRAMEWORKS, ...present])].sort();
1800
+ if (!known.includes(framework)) {
1823
1801
  return {
1824
1802
  ok: false,
1825
- error: `Invalid --framework value: "${p.framework}". Valid: ${[...known].join(', ')}.`,
1803
+ error: `Invalid endpoints framework "${p.framework}". Valid: ${known.join(', ')}.`,
1826
1804
  };
1827
1805
  }
1828
1806
  result.routes = result.routes.filter(route => route.framework === framework);
@@ -2208,8 +2186,8 @@ const HANDLERS = {
2208
2186
  const err = requireFile(p.file);
2209
2187
  if (err) return { ok: false, error: err };
2210
2188
  const result = index.graph(p.file, {
2211
- direction: p.direction || 'both',
2212
- maxDepth: num(p.depth, 2),
2189
+ direction: p.direction || paramDefault('direction', 'deps'),
2190
+ maxDepth: num(p.depth, paramDefault('depth', 'deps')),
2213
2191
  });
2214
2192
  if (result && result.error === 'invalid-direction') {
2215
2193
  return { ok: false, error: result.message };
@@ -2275,8 +2253,23 @@ const HANDLERS = {
2275
2253
  if (classErr) return { ok: false, error: classErr };
2276
2254
  const pinErr = checkDefinitionPin(index, p);
2277
2255
  if (pinErr) return { ok: false, error: pinErr };
2256
+ if (p.delete) {
2257
+ // The same eligibility rule the dead-code audit runs over every
2258
+ // name, asked about this one declaration (core/deletion.js).
2259
+ const resolved = index.resolveSymbol(p.name, {
2260
+ file: p.file, className: p.className, line: p.line,
2261
+ });
2262
+ if (!resolved.def) return { ok: false, error: symbolNotFound(index, p.name) };
2263
+ const assessment = require('./deletion').assessDeletion(index, resolved.def);
2264
+ return { ok: true, result: {
2265
+ found: true, operation: 'delete', ...assessment,
2266
+ // Which declaration a bare name selected, and the others it
2267
+ // could have meant: the verdict is about one of them.
2268
+ ...(resolved.warnings.length > 0 && { warnings: resolved.warnings }),
2269
+ } };
2270
+ }
2278
2271
  if (!p.addParam && !p.removeParam && !p.renameTo) {
2279
- return { ok: false, error: 'Plan requires an operation: add_param, remove_param, or rename_to.' };
2272
+ return { ok: false, error: 'Plan requires an operation: add_param, remove_param, rename_to, or delete.' };
2280
2273
  }
2281
2274
  const result = index.plan(p.name, {
2282
2275
  addParam: p.addParam,
@@ -2296,11 +2289,39 @@ const HANDLERS = {
2296
2289
  },
2297
2290
 
2298
2291
  diffImpact: (index, p) => {
2299
- let result = index.diffImpact({
2300
- base: p.base || 'HEAD',
2301
- staged: p.staged || false,
2302
- file: p.file,
2303
- });
2292
+ // A staged diff describes the git index: its new side is read from
2293
+ // the tree that holds it (core/snapshot.js), never from a worktree
2294
+ // that has moved on.
2295
+ const { resolveTarget, rebasePaths } = require('./snapshot');
2296
+ let target = null;
2297
+ try {
2298
+ target = resolveTarget(index, { staged: !!p.staged });
2299
+ } catch (e) {
2300
+ if (!e || e.code !== 'staged-snapshot-unavailable') throw e;
2301
+ // Outside a repository the diff itself says so, below. Anything
2302
+ // else is a staged tree that could not be read: an answer from
2303
+ // the worktree would be an answer about another tree.
2304
+ if (e.reason !== 'not-a-repository') {
2305
+ return { ok: false, error: `The staged tree could not be exported and indexed (${e.message}); nothing was analyzed.` };
2306
+ }
2307
+ }
2308
+ let result;
2309
+ try {
2310
+ const view = target ? target.index : index;
2311
+ result = view.diffImpact({
2312
+ base: p.base || paramDefault('base'),
2313
+ staged: p.staged || false,
2314
+ file: p.file,
2315
+ all: !!p.all,
2316
+ top: num(p.limit, undefined) || num(p.top, undefined),
2317
+ ...(target && { repoRoot: target.repoRoot }),
2318
+ });
2319
+ // Paths of an exported snapshot are reported where the
2320
+ // repository has them.
2321
+ if (target && target.ephemeral) rebasePaths(result, target.index.root, target.repoRoot);
2322
+ } finally {
2323
+ if (target) target.dispose();
2324
+ }
2304
2325
  const limit = num(p.limit, undefined);
2305
2326
  let note;
2306
2327
  if (limit && limit > 0 && result) {
@@ -2373,7 +2394,7 @@ const HANDLERS = {
2373
2394
  if (fileErr) return { ok: false, error: fileErr };
2374
2395
  }
2375
2396
  // Apply limit to api results (api returns an array)
2376
- const limit = listingLimit(p);
2397
+ const limit = listingLimit(p, 'api');
2377
2398
  let note;
2378
2399
  if (limit && limit > 0 && Array.isArray(result)) {
2379
2400
  const { items, total, limited } = applyLimit(result, limit);
@@ -2563,16 +2584,6 @@ function execute(index, command, params = {}) {
2563
2584
  }
2564
2585
  const response = handler(index, params);
2565
2586
  response.projectRoot = index.root;
2566
- const bundled = (index.discoveryIssues || []).filter(issue => issue.reason === 'bundled');
2567
- if (bundled.length > 0) {
2568
- const files = bundled.slice(0, 5).map(issue => issue.relativePath).join(', ');
2569
- const note = `UCN skipped ${bundled.length} bundled/minified or source-map file(s): ${files}` +
2570
- (bundled.length > 5 ? ', ...' : '') +
2571
- '. This is not a repository-wide semantic zero; use --include-bundled for JavaScript bundles ' +
2572
- '(source maps remain unindexed), or grep/ripgrep to inspect skipped files.';
2573
- if (response.ok) response.note = combineNotes([response.note, note]);
2574
- else response.error = `${response.error}\n${note}`;
2575
- }
2576
2587
  return response;
2577
2588
  } catch (e) {
2578
2589
  // A deliberate refusal keeps its message; an exception inside the