ucn 4.2.3 → 5.0.2

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 (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { CANONICAL_COMMANDS } = require('./registry');
4
+
5
+ const NAME_COMMANDS = new Set(['show', 'find', 'usages', 'trace', 'tests', 'plan']);
6
+
7
+ function parseSourceTarget(arg, params) {
8
+ if (params.range) return { ...params, file: params.file, range: params.range };
9
+ const value = String(arg || '').trim();
10
+ const twoPart = value.match(/^(.+?)\s+(\d+(?:-\d+)?)$/);
11
+ if (twoPart) return { ...params, file: params.file || twoPart[1], range: twoPart[2] };
12
+ // A single-colon file:range target is a line request. Stable symbol
13
+ // handles with a name contain a second colon and stay symbol targets.
14
+ const inline = value.match(/^([^:]+(?:\/[^:]*)?):(\d+(?:-\d+)?)$/);
15
+ if (inline) return { ...params, file: params.file || inline[1], range: inline[2] };
16
+ return { ...params, name: arg };
17
+ }
18
+
19
+ /** Build normalized execute() params from the shared CLI-style positional arg. */
20
+ function buildPublicParams(command, arg, params = {}) {
21
+ const clean = { ...params };
22
+ if (clean.top === 0 && params.topRaw == null) delete clean.top;
23
+ if (clean.maxLines == null) delete clean.maxLines;
24
+ // Surface-only values never belong in execute params.
25
+ for (const key of ['json', 'quiet', 'cache', 'clearCache', 'followSymlinks', 'interactive',
26
+ 'topRaw', 'limitRaw', 'maxFilesRaw', 'maxLinesRaw', 'maxChars', 'maxCharsRaw',
27
+ 'depthRaw', 'contextRaw', 'workersRaw',
28
+ '_fileFromFileMode']) delete clean[key];
29
+
30
+ if (NAME_COMMANDS.has(command)) return { ...clean, name: arg || clean.name };
31
+ switch (command) {
32
+ case 'search': return { ...clean, term: arg || clean.term };
33
+ case 'source': return parseSourceTarget(arg || clean.name, clean);
34
+ case 'impact': return { ...clean, ...(arg ? { name: arg } : {}) };
35
+ case 'deps': return { ...clean, file: arg || clean.file };
36
+ case 'api': return { ...clean, file: arg || clean.file };
37
+ case 'check': return { ...clean, ...(arg ? { name: arg } : {}) };
38
+ case 'stacktrace': return { ...clean, stack: clean.stack || arg };
39
+ default: return clean;
40
+ }
41
+ }
42
+
43
+ function isPublicCommand(command) {
44
+ return CANONICAL_COMMANDS.includes(command);
45
+ }
46
+
47
+ module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget };
package/core/registry.js CHANGED
@@ -11,56 +11,81 @@
11
11
  // CANONICAL COMMANDS
12
12
  // ============================================================================
13
13
 
14
- // All commands using camelCase canonical IDs.
15
- // Order: understanding, finding, extracting, file-deps, refactoring, other.
14
+ // Public v5 commands using camelCase canonical IDs.
15
+ //
16
+ // The engine still exposes narrower internal operations (about/context/blast,
17
+ // etc.) through execute.js so their well-tested analysis primitives can be
18
+ // composed. CLI and MCP are derived exclusively from this list: the public
19
+ // surface is intentionally small and has no legacy command aliases.
16
20
  const CANONICAL_COMMANDS = [
17
- // Understanding code
18
- 'about', 'context', 'impact', 'blast', 'smart', 'trace', 'reverseTrace', 'example', 'related', 'brief',
19
- // Finding code
20
- 'find', 'usages', 'toc', 'search', 'tests', 'affectedTests', 'deadcode', 'entrypoints', 'endpoints',
21
- // Extracting code
22
- 'fn', 'class', 'lines', 'expand',
23
- // File dependencies
24
- 'imports', 'exporters', 'fileExports', 'graph', 'circularDeps',
25
- // Refactoring
26
- 'verify', 'plan', 'diffImpact', 'check',
27
- // Other
28
- 'typedef', 'stacktrace', 'api', 'stats', 'doctor', 'auditAsync', 'orient',
21
+ // Understand and navigate
22
+ 'show', 'find', 'usages', 'search', 'source', 'trace',
23
+ // Change and validation
24
+ 'impact', 'tests', 'check', 'plan',
25
+ // Repository and architecture
26
+ 'repo', 'deps', 'api', 'entrypoints', 'endpoints',
27
+ // Focused audits / runtime evidence
28
+ 'deadcode', 'auditAsync', 'stacktrace',
29
29
  ];
30
30
 
31
- // ============================================================================
32
- // COMMAND ALIASES (surface-specific → canonical)
33
- // ============================================================================
34
-
35
- // CLI uses hyphenated multi-word names plus legacy aliases.
36
- const CLI_ALIASES = {
37
- 'file-exports': 'fileExports',
38
- 'what-exports': 'fileExports',
39
- 'diff-impact': 'diffImpact',
40
- 'what-imports': 'imports',
41
- 'who-imports': 'exporters',
42
- 'stack': 'stacktrace',
43
- 'affected': 'affectedTests',
44
- 'affected-tests': 'affectedTests',
45
- 'reverse-trace': 'reverseTrace',
46
- 'rtrace': 'reverseTrace',
47
- 'circular-deps': 'circularDeps',
48
- 'circular': 'circularDeps',
49
- 'cycles': 'circularDeps',
50
- 'audit-async': 'auditAsync',
51
- // BUG-3: parity with other multi-word commands (circular-deps, reverse-trace, ...)
52
- 'entry-points': 'entrypoints',
31
+ // Directive guidance for retired v4 commands. These names remain invalid —
32
+ // this is not a compatibility execution layer — but agents receive the exact
33
+ // v5 command that answers the old intent instead of a dead end.
34
+ const V4_COMMAND_MIGRATIONS = {
35
+ about: { purpose: 'For a symbol summary', cli: 'ucn show <name>', mcp: 'command "show" with name' },
36
+ context: { purpose: 'For callers and callees', cli: 'ucn show <name>', mcp: 'command "show" with name' },
37
+ smart: { purpose: 'For source plus callees', cli: 'ucn show <name> --sections=source,callees', mcp: 'command "show" with name, sections="source,callees"' },
38
+ related: { purpose: 'For related symbols', cli: 'ucn show <name> --sections=related', mcp: 'command "show" with name, sections="related"' },
39
+ example: { purpose: 'For a call-site example', cli: 'ucn show <name> --sections=example', mcp: 'command "show" with name, sections="example"' },
40
+ brief: { purpose: 'For a compact symbol summary', cli: 'ucn show <name> --compact', mcp: 'command "show" with name, compact=true' },
41
+ callers: { purpose: 'For who calls a symbol', cli: 'ucn show <name> --sections=callers', mcp: 'command "show" with name, sections="callers"' },
42
+ callees: { purpose: 'For what a symbol calls', cli: 'ucn show <name> --sections=callees', mcp: 'command "show" with name, sections="callees"' },
43
+ typedef: { purpose: 'For a type definition', cli: 'ucn find <name> --type=type', mcp: 'command "find" with name, type="type"' },
44
+ blast: { purpose: 'For the transitive caller tree', cli: 'ucn trace <name> --direction=callers', mcp: 'command "trace" with name, direction="callers"' },
45
+ reverseTrace: { purpose: 'For entry-point paths', cli: 'ucn trace <name> --direction=callers --to=entrypoints', mcp: 'command "trace" with name, direction="callers", to="entrypoints"' },
46
+ toc: { purpose: 'For the per-file symbol listing', cli: 'ucn repo --sections=files', mcp: 'command "repo" with sections="files"' },
47
+ stats: { purpose: 'For project statistics', cli: 'ucn repo --sections=stats', mcp: 'command "repo" with sections="stats"' },
48
+ doctor: { purpose: 'For index health and trust', cli: 'ucn repo --sections=health --deep', mcp: 'command "repo" with sections="health", deep=true' },
49
+ orient: { purpose: 'For repository orientation', cli: 'ucn repo', mcp: 'command "repo"' },
50
+ affectedTests: { purpose: 'For transitively affected tests', cli: 'ucn tests <name> --depth=<n>', mcp: 'command "tests" with name, depth=<n>' },
51
+ fn: { purpose: 'To extract a function', cli: 'ucn source <name>', mcp: 'command "source" with name' },
52
+ class: { purpose: 'To extract a class', cli: 'ucn source <name>', mcp: 'command "source" with name' },
53
+ lines: { purpose: 'To extract a line range', cli: 'ucn source <file> --range=<start-end>', mcp: 'command "source" with name=<file>, range="<start-end>"' },
54
+ expand: { purpose: 'To expand a listed item', cli: 'ucn source <handle>', mcp: 'command "source" with name=<handle>' },
55
+ imports: { purpose: 'For what a file imports', cli: 'ucn deps <file> --direction=imports', mcp: 'command "deps" with file, direction="imports"' },
56
+ exporters: { purpose: 'For who imports a file', cli: 'ucn deps <file> --direction=importers', mcp: 'command "deps" with file, direction="importers"' },
57
+ graph: { purpose: 'For a dependency graph', cli: 'ucn deps <file> --depth=<n>', mcp: 'command "deps" with file, depth=<n>' },
58
+ circularDeps: { purpose: 'For circular dependencies', cli: 'ucn deps --cycles', mcp: 'command "deps" with cycles=true' },
59
+ fileExports: { purpose: 'For a file public surface', cli: 'ucn api <file>', mcp: 'command "api" with file' },
60
+ verify: { purpose: 'To validate call sites', cli: 'ucn check <name>', mcp: 'command "check" with name' },
61
+ diffImpact: { purpose: 'For Git-diff impact', cli: 'ucn impact --base <ref>', mcp: 'command "impact" with base' },
62
+ stack: { purpose: 'To analyze a stack trace', cli: 'ucn stacktrace "<paste>"', mcp: 'command "stacktrace" with stack' },
53
63
  };
54
64
 
55
- // MCP uses snake_case for multi-word names.
56
- const MCP_ALIASES = {
57
- 'file_exports': 'fileExports',
58
- 'diff_impact': 'diffImpact',
59
- 'affected_tests': 'affectedTests',
60
- 'reverse_trace': 'reverseTrace',
61
- 'circular_deps': 'circularDeps',
62
- 'audit_async': 'auditAsync',
65
+ const V4_SPELLING_ALIASES = {
66
+ rtrace: 'reverseTrace', affected: 'affectedTests',
67
+ circular: 'circularDeps', cycles: 'circularDeps',
68
+ 'what-exports': 'fileExports', 'what-imports': 'imports',
69
+ 'who-imports': 'exporters',
63
70
  };
71
+ const V4_MIGRATION_LOOKUP = new Map();
72
+ {
73
+ const normalize = value => String(value).toLowerCase().replace(/[-_]/g, '');
74
+ for (const [name, entry] of Object.entries(V4_COMMAND_MIGRATIONS)) {
75
+ V4_MIGRATION_LOOKUP.set(normalize(name), entry);
76
+ }
77
+ for (const [alias, name] of Object.entries(V4_SPELLING_ALIASES)) {
78
+ V4_MIGRATION_LOOKUP.set(normalize(alias), V4_COMMAND_MIGRATIONS[name]);
79
+ }
80
+ }
81
+
82
+ function v4MigrationHint(name, surface = 'cli') {
83
+ if (!name || resolveCommand(String(name), surface)) return null;
84
+ const entry = V4_MIGRATION_LOOKUP.get(
85
+ String(name).toLowerCase().replace(/[-_]/g, ''));
86
+ if (!entry) return null;
87
+ return `${entry.purpose}, use: ${surface === 'mcp' ? entry.mcp : entry.cli}.`;
88
+ }
64
89
 
65
90
  // ============================================================================
66
91
  // PARAM NORMALIZATION (snake_case → camelCase)
@@ -71,7 +96,6 @@ const PARAM_MAP = {
71
96
  include_tests: 'includeTests',
72
97
  exclude_tests: 'excludeTests',
73
98
  include_methods: 'includeMethods',
74
- include_uncertain: 'includeUncertain',
75
99
  with_types: 'withTypes',
76
100
  code_only: 'codeOnly',
77
101
  case_sensitive: 'caseSensitive',
@@ -79,7 +103,6 @@ const PARAM_MAP = {
79
103
  include_decorated: 'includeDecorated',
80
104
  min_confidence: 'minConfidence',
81
105
  show_confidence: 'showConfidence',
82
- hide_confidence: 'hideConfidence',
83
106
  calls_only: 'callsOnly',
84
107
  class_name: 'className',
85
108
  max_lines: 'maxLines',
@@ -96,6 +119,7 @@ const PARAM_MAP = {
96
119
  client_only: 'clientOnly',
97
120
  hide_uncertain: 'hideUncertain',
98
121
  expand_unverified: 'expandUnverified',
122
+ with_source: 'withSource',
99
123
  };
100
124
 
101
125
  // ============================================================================
@@ -106,78 +130,39 @@ const PARAM_MAP = {
106
130
  // MCP param stripping, CLI inapplicable-flag warnings, and architecture guards.
107
131
  // file* = file is the command subject (required), not a filter pattern.
108
132
  const FLAG_APPLICABILITY = {
109
- // Understanding code
110
- about: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git'],
111
- // Note: includeMethods/includeUncertain are deprecated no-ops for
112
- // about/context/impact since the tiered-output contract (unverified
113
- // callers are always shown in their own section); kept in the matrix so
114
- // legacy invocations don't warn as "inapplicable". `all` lifts the
115
- // unverified display cap.
116
- context: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'all'],
117
- impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'top', 'unreachableOnly', 'compact'],
118
- // trace/blast/reverseTrace/affectedTests run the tiered tree contract:
119
- // includeUncertain is an implied no-op (unverified edges are always
120
- // visible — frontier/possible band); expandUnverified follows unverified
121
- // CALLER edges, marking downstream nodes chainUnverified (blast/
122
- // reverseTrace only — surface trace is down-direction, where unresolved
123
- // callees have no definition to expand into).
124
- blast: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
125
- reverseTrace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence', 'expandUnverified'],
126
- smart: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'withTypes', 'minConfidence'],
127
- trace: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'all', 'minConfidence'],
128
- example: ['name', 'file', 'className', 'line', 'diverse', 'top', 'includeTests'],
129
- related: ['name', 'file', 'className', 'line', 'top', 'all'],
130
- brief: ['name', 'file', 'className', 'line', 'git'],
131
- // Finding code
132
- find: ['name', 'file', 'exclude', 'className', 'includeTests', 'top', 'limit', 'exact', 'in', 'all', 'depth', 'compact'],
133
- usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact'],
134
- toc: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in'],
133
+ // Understand one symbol. `sections` is a comma-separated projection:
134
+ // summary, callers, callees, source, dependencies, tests, types, example,
135
+ // related. Caller-bearing projections always preserve ACCOUNT/CONTRACT.
136
+ show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse'],
137
+ find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource'],
138
+ usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all'],
135
139
  search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
136
- tests: ['name', 'file', 'exclude', 'className', 'callsOnly'],
137
- affectedTests:['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'includeUncertain', 'depth', 'minConfidence'],
140
+ source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines'],
141
+ trace: ['name', 'file', 'exclude', 'className', 'line', 'direction', 'to', 'includeMethods', 'depth', 'all', 'expandUnverified'],
142
+ impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all'],
143
+ tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
144
+ deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
145
+ api: ['file', 'in', 'limit'],
146
+ check: ['name', 'file', 'className', 'line', 'includeMethods', 'base', 'staged', 'limit'],
147
+ plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
148
+ repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
138
149
  deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
139
150
  entrypoints: ['file', 'exclude', 'includeTests', 'excludeTests', 'limit', 'type', 'framework'],
140
151
  endpoints: ['file', 'exclude', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
141
- // Extracting code
142
- fn: ['name', 'file', 'className', 'line', 'all'],
143
- class: ['name', 'file', 'line', 'all', 'maxLines'],
144
- lines: ['file', 'range'],
145
- expand: ['item'],
146
- // File dependencies
147
- imports: ['file'],
148
- exporters: ['file'],
149
- fileExports: ['file'],
150
- graph: ['file', 'depth', 'direction', 'all'],
151
- circularDeps: ['file', 'exclude'],
152
- // Refactoring
153
- // verify runs the tiered caller contract (v4): includeMethods/
154
- // includeUncertain are implied no-ops (unverified sites always visible in
155
- // their own band); kept in the matrix so legacy invocations don't warn.
156
- verify: ['name', 'file', 'className', 'line', 'includeMethods', 'includeUncertain'],
157
- plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
158
- diffImpact: ['file', 'limit', 'base', 'staged', 'all'],
159
- check: ['file', 'base', 'staged', 'limit'],
160
- // Other
161
- typedef: ['name', 'file', 'className', 'exact'],
162
152
  stacktrace: ['stack'],
163
- api: ['file', 'limit'],
164
- stats: ['functions', 'hot', 'top'],
165
- doctor: ['file', 'in', 'deep'],
166
- orient: ['top'],
167
153
  auditAsync: ['file', 'exclude', 'limit'],
168
154
  };
169
155
 
170
156
  // Commands whose output is project-wide — truncation means you need a filter, not more text.
171
157
  // Used by MCP server for tighter default output limits.
172
158
  const BROAD_COMMANDS = new Set([
173
- 'toc', 'entrypoints', 'endpoints', 'diffImpact', 'affectedTests',
174
- 'deadcode', 'usages', 'reverseTrace', 'circularDeps',
175
- 'doctor', 'check', 'auditAsync', 'orient',
159
+ 'repo', 'entrypoints', 'endpoints', 'tests', 'deadcode', 'usages',
160
+ 'deps', 'check', 'auditAsync',
176
161
  ]);
177
162
 
178
163
  // Commands that can operate on a single file without a project index.
179
164
  // Used by CLI to decide whether to build a file-local or project-wide index.
180
- const FILE_LOCAL_COMMANDS = new Set(['toc', 'fn', 'class', 'find', 'usages', 'search', 'lines', 'typedef', 'api']);
165
+ const FILE_LOCAL_COMMANDS = new Set(['find', 'usages', 'search', 'source', 'api']);
181
166
 
182
167
  // ============================================================================
183
168
  // HELPERS
@@ -186,16 +171,60 @@ const FILE_LOCAL_COMMANDS = new Set(['toc', 'fn', 'class', 'find', 'usages', 'se
186
171
  /**
187
172
  * Resolve a surface-specific command name to its canonical ID.
188
173
  *
189
- * @param {string} name - Command name as used by the surface (e.g. 'diff-impact', 'diff_impact')
190
- * @param {'cli'|'mcp'} [surface='cli'] - Which surface's aliases to check first
174
+ * @param {string} name - Command name as used by the surface (e.g. 'audit-async', 'audit_async')
175
+ * @param {'cli'|'mcp'} [surface='cli'] - Surface spelling to resolve
191
176
  * @returns {string|null} Canonical command ID, or null if unknown
192
177
  */
193
178
  function resolveCommand(name, surface) {
194
179
  if (CANONICAL_COMMANDS.includes(name)) return name;
195
180
  if (surface === 'mcp') {
196
- return MCP_ALIASES[name] || CLI_ALIASES[name] || null;
181
+ return CANONICAL_COMMANDS.find(cmd => toMcpName(cmd) === name) || null;
197
182
  }
198
- return CLI_ALIASES[name] || null;
183
+ return CANONICAL_COMMANDS.find(cmd => toCliName(cmd) === name) || null;
184
+ }
185
+
186
+ function editDistance(left, right) {
187
+ const a = String(left || '').toLowerCase();
188
+ const b = String(right || '').toLowerCase();
189
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
190
+ for (let i = 1; i <= a.length; i++) {
191
+ let diagonal = row[0];
192
+ row[0] = i;
193
+ for (let j = 1; j <= b.length; j++) {
194
+ const above = row[j];
195
+ row[j] = Math.min(
196
+ row[j] + 1,
197
+ row[j - 1] + 1,
198
+ diagonal + (a[i - 1] === b[j - 1] ? 0 : 1),
199
+ );
200
+ diagonal = above;
201
+ }
202
+ }
203
+ return row[b.length];
204
+ }
205
+
206
+ /**
207
+ * Return a single high-confidence correction for a misspelled public command.
208
+ * The threshold is deliberately strict because the CLI's first positional can
209
+ * also be a project path; an unrelated missing path must not become a command.
210
+ */
211
+ function suggestCommand(name, surface = 'cli') {
212
+ if (!name) return null;
213
+ const spellings = CANONICAL_COMMANDS.map(command => ({
214
+ command,
215
+ spelling: surface === 'mcp' ? toMcpName(command) : toCliName(command),
216
+ }));
217
+ const ranked = spellings
218
+ .map(row => ({ ...row, distance: editDistance(name, row.spelling) }))
219
+ // Code-unit tiebreak (rule 11) — registry stays require-free, so the
220
+ // comparison is inlined instead of importing shared.codeUnitCompare.
221
+ .sort((a, b) => a.distance - b.distance ||
222
+ (a.spelling < b.spelling ? -1 : a.spelling > b.spelling ? 1 : 0));
223
+ const best = ranked[0];
224
+ const threshold = String(name).length <= 4 ? 2 : Math.min(3, Math.floor(String(name).length / 3));
225
+ if (!best || best.distance > threshold) return null;
226
+ if (ranked[1] && ranked[1].distance === best.distance) return null;
227
+ return best.spelling;
199
228
  }
200
229
 
201
230
  /**
@@ -215,8 +244,7 @@ function normalizeParams(params) {
215
244
  // ============================================================================
216
245
 
217
246
  /**
218
- * Generate the CLI COMMANDS set (canonical names + all CLI aliases).
219
- * Includes hyphenated forms and legacy aliases.
247
+ * Generate the exact CLI command set using hyphenated surface spelling.
220
248
  */
221
249
  function getCliCommandSet() {
222
250
  const set = new Set();
@@ -227,11 +255,6 @@ function getCliCommandSet() {
227
255
  set.add(hyphenated);
228
256
  }
229
257
 
230
- // Add legacy aliases
231
- for (const alias of Object.keys(CLI_ALIASES)) {
232
- set.add(alias);
233
- }
234
-
235
258
  return set;
236
259
  }
237
260
 
@@ -260,6 +283,67 @@ function toCliName(canonical) {
260
283
  return canonical.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
261
284
  }
262
285
 
286
+ function formatSurfaceMessage(message, surface = 'cli') {
287
+ let rendered = String(message || '');
288
+ for (const [snake, camel] of Object.entries(PARAM_MAP)) {
289
+ const spellings = [snake, camel];
290
+ const target = surface === 'mcp'
291
+ ? snake
292
+ : `--${snake.replace(/_/g, '-')}`;
293
+ for (const spelling of spellings) {
294
+ rendered = rendered.replace(
295
+ new RegExp(`(?<![A-Za-z0-9_-])${spelling}(?![A-Za-z0-9_-])`, 'g'),
296
+ target,
297
+ );
298
+ }
299
+ }
300
+ const knownParams = new Set(Object.values(FLAG_APPLICABILITY).flat());
301
+ const booleanParams = new Set([
302
+ 'includeTests', 'excludeTests', 'includeMethods', 'withTypes',
303
+ 'codeOnly', 'caseSensitive', 'includeExported', 'includeDecorated',
304
+ 'showConfidence', 'callsOnly', 'topLevel', 'followSymlinks',
305
+ 'unreachableOnly', 'serverOnly', 'clientOnly', 'hideUncertain',
306
+ 'expandUnverified', 'withSource', 'all', 'compact', 'exact',
307
+ 'regex', 'exported', 'unused', 'staged', 'detailed', 'functions',
308
+ 'hot', 'deep', 'cycles', 'bridge', 'unmatched', 'diverse', 'git',
309
+ ]);
310
+ if (surface === 'mcp') {
311
+ rendered = rendered.replace(/--([a-z][a-z0-9-]*)(?:=([^\s,.)]+))?/g,
312
+ (whole, flag, rawValue) => {
313
+ const camel = flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
314
+ if (!knownParams.has(camel) && !['maxChars', 'maxFiles'].includes(camel)) {
315
+ return whole;
316
+ }
317
+ const snake = REVERSE_PARAM_MAP[camel] || camel;
318
+ if (rawValue != null) {
319
+ const value = rawValue === 'N' ? '<n>' : rawValue;
320
+ return `${snake}=${value}`;
321
+ }
322
+ return booleanParams.has(camel) ? `${snake}=true` : snake;
323
+ });
324
+ rendered = rendered
325
+ .replace(/\b(line|top|limit|depth|max_lines|max_files|max_chars)=(?=\s|[,.]|$)/g, '$1=<n>')
326
+ .replace(/\b(file|in|exclude)=(?=\s|[,.]|$)/g, '$1=<path>')
327
+ .replace(/\bclass_name=(?=\s|[,.]|$)/g, 'class_name=<name>');
328
+ } else {
329
+ // Single-word parameters are intentionally absent from PARAM_MAP.
330
+ // Translate them only in parameter syntax (`file=`, not prose "file").
331
+ for (const param of knownParams) {
332
+ if (REVERSE_PARAM_MAP[param] || Object.values(PARAM_MAP).includes(param)) continue;
333
+ rendered = rendered.replace(
334
+ new RegExp(`(?<![A-Za-z0-9_-])${param}(?=\\s*=)`, 'g'),
335
+ `--${param.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`,
336
+ );
337
+ }
338
+ rendered = rendered.replace(/(--[a-z][a-z0-9-]*)=true\b/g, '$1');
339
+ rendered = rendered
340
+ .replace(/--(line|top|limit|depth|max-lines|max-files|max-chars)=(?=\s|[,.]|$)/g, '--$1=N')
341
+ .replace(/--(file|in|exclude)=(?=\s|[,.]|$)/g, '--$1=<path>')
342
+ .replace(/--class-name=(?=\s|[,.]|$)/g, '--class-name=<name>');
343
+ }
344
+ return rendered;
345
+ }
346
+
263
347
  /**
264
348
  * Build a reverse map: camelCase → snake_case from PARAM_MAP.
265
349
  * Flags not in PARAM_MAP are already snake_case-safe (single words).
@@ -274,10 +358,51 @@ function buildReverseParamMap() {
274
358
 
275
359
  const REVERSE_PARAM_MAP = buildReverseParamMap();
276
360
 
361
+ const CLI_GLOBAL_FLAGS = Object.freeze([
362
+ '--help', '-h', '--version', '-v', '--mcp',
363
+ '--json', '--verbose', '--no-quiet', '--quiet',
364
+ '--interactive', '-i',
365
+ '--no-cache', '--clear-cache', '--no-follow-symlinks',
366
+ '--max-files', '--max-chars', '--workers',
367
+ ]);
368
+
369
+ const CLI_PARAM_FLAG_OVERRIDES = Object.freeze({
370
+ exclude: ['--exclude', '--not'],
371
+ includeMethods: ['--include-methods', '--no-include-methods'],
372
+ regex: ['--regex', '--no-regex'],
373
+ showConfidence: ['--show-confidence', '--hide-confidence', '--no-confidence'],
374
+ hideUncertain: ['--hide-uncertain', '--no-uncertain'],
375
+ compact: ['--compact', '--no-compact'],
376
+ defaultValue: ['--default-value', '--default'],
377
+ });
378
+
379
+ function cliFlagsForParam(param) {
380
+ if (CLI_PARAM_FLAG_OVERRIDES[param]) {
381
+ return CLI_PARAM_FLAG_OVERRIDES[param];
382
+ }
383
+ const snake = REVERSE_PARAM_MAP[param] || param;
384
+ return [`--${snake.replace(/_/g, '-')}`];
385
+ }
386
+
387
+ function getCliFlagsForCommand(command) {
388
+ const canonical = resolveCommand(command, 'cli') || command;
389
+ return [...new Set((FLAG_APPLICABILITY[canonical] || [])
390
+ .filter(param => !['name', 'term'].includes(param))
391
+ .flatMap(cliFlagsForParam))];
392
+ }
393
+
394
+ function getCliAcceptedFlags() {
395
+ const accepted = new Set(CLI_GLOBAL_FLAGS);
396
+ for (const command of CANONICAL_COMMANDS) {
397
+ for (const flag of getCliFlagsForCommand(command)) accepted.add(flag);
398
+ }
399
+ return accepted;
400
+ }
401
+
277
402
  /**
278
403
  * Generate per-command parameter listing for the MCP tool description.
279
404
  * Maps camelCase flags back to snake_case for MCP clients.
280
- * One line per command: `about: file, exclude, class_name, ...`
405
+ * One line per command: `show: file, exclude, class_name, ...`
281
406
  */
282
407
  function generateMcpParamSection() {
283
408
  const lines = ['', 'ACCEPTED FLAGS PER COMMAND (max_chars, max_files, follow_symlinks always accepted; flags not listed below are ignored):'];
@@ -293,18 +418,23 @@ function generateMcpParamSection() {
293
418
 
294
419
  module.exports = {
295
420
  CANONICAL_COMMANDS,
296
- CLI_ALIASES,
297
- MCP_ALIASES,
421
+ V4_COMMAND_MIGRATIONS,
422
+ v4MigrationHint,
298
423
  PARAM_MAP,
299
424
  REVERSE_PARAM_MAP,
300
425
  FLAG_APPLICABILITY,
301
426
  BROAD_COMMANDS,
302
427
  FILE_LOCAL_COMMANDS,
303
428
  resolveCommand,
429
+ suggestCommand,
304
430
  normalizeParams,
305
431
  getCliCommandSet,
306
432
  getMcpCommandEnum,
307
433
  toMcpName,
308
434
  toCliName,
435
+ formatSurfaceMessage,
436
+ cliFlagsForParam,
437
+ getCliFlagsForCommand,
438
+ getCliAcceptedFlags,
309
439
  generateMcpParamSection,
310
440
  };