ucn 5.5.3 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -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 +474 -0
  6. package/README.md +69 -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 +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/command-contracts.js +7 -6
  19. package/core/constructed-class.js +1195 -0
  20. package/core/contract-membership.js +142 -0
  21. package/core/deadcode.js +362 -84
  22. package/core/deletion.js +401 -0
  23. package/core/discovery.js +104 -9
  24. package/core/entrypoints.js +12 -6
  25. package/core/evidence-bands.js +68 -0
  26. package/core/evidence-bands.json +44 -0
  27. package/core/execute.js +172 -161
  28. package/core/hot-summary.js +241 -0
  29. package/core/index-ir.js +3 -1
  30. package/core/ir.js +5 -1
  31. package/core/member-signatures.js +789 -0
  32. package/core/output/analysis-ext.js +20 -10
  33. package/core/output/analysis.js +98 -60
  34. package/core/output/check.js +98 -37
  35. package/core/output/doctor.js +34 -1
  36. package/core/output/find.js +2 -1
  37. package/core/output/lines.js +63 -3
  38. package/core/output/public.js +29 -11
  39. package/core/output/refactoring.js +32 -0
  40. package/core/output/reporting.js +15 -2
  41. package/core/output/search.js +11 -2
  42. package/core/output/tracing.js +35 -7
  43. package/core/output-budget.js +124 -57
  44. package/core/parallel-build.js +3 -3
  45. package/core/project.js +106 -23
  46. package/core/provenance-type.js +12 -5
  47. package/core/public-command.js +35 -9
  48. package/core/python-modules.js +6 -0
  49. package/core/reflection.js +151 -23
  50. package/core/registry.js +623 -72
  51. package/core/reporting.js +69 -34
  52. package/core/review-items.js +75 -0
  53. package/core/search.js +89 -128
  54. package/core/shared.js +0 -45
  55. package/core/snapshot.js +350 -0
  56. package/core/source-universe.js +707 -0
  57. package/core/test-units.js +3624 -0
  58. package/core/tracing.js +150 -96
  59. package/core/verify.js +27 -11
  60. package/languages/adapter.js +8 -3
  61. package/languages/c-family.js +12 -0
  62. package/languages/csharp.js +634 -0
  63. package/languages/go.js +120 -0
  64. package/languages/index.js +422 -4
  65. package/languages/java.js +712 -0
  66. package/languages/javascript.js +1460 -2
  67. package/languages/lexical-scope.js +68 -0
  68. package/languages/python.js +1672 -1
  69. package/languages/rust.js +91 -0
  70. package/mcp/server.js +44 -110
  71. package/package.json +10 -5
package/core/registry.js CHANGED
@@ -88,41 +88,291 @@ function v4MigrationHint(name, surface = 'cli') {
88
88
  }
89
89
 
90
90
  // ============================================================================
91
- // PARAM NORMALIZATION (snake_case → camelCase)
91
+ // PARAMETER TABLE
92
92
  // ============================================================================
93
93
 
94
- const PARAM_MAP = {
95
- project_dir: 'projectDir',
96
- include_tests: 'includeTests',
97
- exclude_tests: 'excludeTests',
98
- include_methods: 'includeMethods',
99
- with_types: 'withTypes',
100
- code_only: 'codeOnly',
101
- case_sensitive: 'caseSensitive',
102
- include_exported: 'includeExported',
103
- include_decorated: 'includeDecorated',
104
- min_confidence: 'minConfidence',
105
- show_confidence: 'showConfidence',
106
- calls_only: 'callsOnly',
107
- class_name: 'className',
108
- max_lines: 'maxLines',
109
- add_param: 'addParam',
110
- remove_param: 'removeParam',
111
- rename_to: 'renameTo',
112
- default_value: 'defaultValue',
113
- top_level: 'topLevel',
114
- max_files: 'maxFiles',
115
- max_chars: 'maxChars',
116
- follow_symlinks: 'followSymlinks',
117
- include_bundled: 'includeBundled',
118
- unreachable_only: 'unreachableOnly',
119
- server_only: 'serverOnly',
120
- client_only: 'clientOnly',
121
- hide_uncertain: 'hideUncertain',
122
- expand_unverified: 'expandUnverified',
123
- with_source: 'withSource',
94
+ // Project/change-wide sweep commands with no required symbol argument:
95
+ // truncation means you need a filter, not more text. Their default output
96
+ // budget is the smaller one in the parameter table (`maxChars`).
97
+ const BROAD_COMMANDS = new Set([
98
+ 'repo', 'entrypoints', 'endpoints', 'deadcode',
99
+ 'deps', 'check', 'auditAsync',
100
+ ]);
101
+ const BROAD_OUTPUT_CHARS = 3000;
102
+
103
+ // Structural search kinds. `find` takes the same kinds except `call`, which
104
+ // is a call site rather than a definition.
105
+ const SEARCH_TYPES = Object.freeze([
106
+ 'function', 'class', 'call', 'method', 'type', 'constructor',
107
+ 'state', 'field', 'constant', 'macro', 'variable',
108
+ 'interface', 'enum', 'struct', 'trait', 'record', 'namespace',
109
+ ]);
110
+ const FIND_TYPES = Object.freeze([
111
+ 'type', 'function', 'class', 'variable', 'state', 'constant',
112
+ 'field', 'macro', 'method', 'constructor', 'interface', 'enum',
113
+ 'struct', 'trait', 'record', 'namespace',
114
+ ]);
115
+
116
+ // Every public parameter, declared once. A surface states nothing about a
117
+ // parameter that is not here: the MCP input schema, the CLI flag parser with
118
+ // its bounds, the help reference and the engine's own validation read this
119
+ // table.
120
+ //
121
+ // type string | boolean | integer | number
122
+ // min, max inclusive bounds of a numeric parameter
123
+ // values the closed set of accepted values: an array, or an object
124
+ // keyed by command where commands differ. A function is called on
125
+ // first use (a set its owning module derives from its own tables).
126
+ // list the value is a comma-separated list of `values`
127
+ // fold 'upper' | 'lower': a value is matched after case folding
128
+ // defaults what the engine uses when the parameter is omitted, keyed by
129
+ // command, `command:mode`, or '*' for every command
130
+ // facts other numbers the description states
131
+ // examples sample values of an open set; every command that takes the
132
+ // parameter accepts each one
133
+ // global accepted by every command (build and transport control)
134
+ // repeat a CLI value flag that may be given several times
135
+ // cli flag spellings where they are not `--<kebab-case name>`:
136
+ // an array for a value, { on, off } for a boolean
137
+ // text the description. {placeholders} are filled from the fields
138
+ // above, so the text itself states no value list and no number.
139
+ //
140
+ // Key order is the property order of the MCP input schema.
141
+ const PARAMS = {
142
+ // `name` and `term` are the positional argument on the CLI: no flag.
143
+ name: { type: 'string', cli: [], text: 'Symbol name or stable path:line:name handle. Used by {commands}.' },
144
+ file: { type: 'string', text: 'File target for source/deps/api or a symbol-disambiguation filter.' },
145
+ sections: {
146
+ type: 'string',
147
+ list: true,
148
+ fold: 'lower',
149
+ values: {
150
+ show: ['summary', 'callers', 'callees', 'source', 'dependencies', 'tests', 'types', 'example', 'related'],
151
+ 'show:lines': ['callers', 'callees'],
152
+ repo: ['summary', 'files', 'stats', 'health'],
153
+ },
154
+ defaults: {
155
+ show: ['summary', 'callers', 'callees'],
156
+ 'show:lines': ['callers'],
157
+ repo: ['summary'],
158
+ 'repo:deep': ['summary', 'health'],
159
+ },
160
+ text: 'Comma-separated projection. {valueSets}. Omitted: {defaults}.',
161
+ },
162
+ exclude: { type: 'string', cli: ['--exclude', '--not'], repeat: true, text: 'Comma-separated patterns to exclude (e.g. "test,mock,vendor")' },
163
+ includeTests: { type: 'boolean', text: 'Include test files in results (excluded when omitted)' },
164
+ excludeTests: { type: 'boolean', text: 'Exclude test files: the default for entrypoints (include_tests=true lists them); usages and endpoints list test files unless exclude_tests=true.' },
165
+ includeMethods: {
166
+ type: 'boolean',
167
+ cli: { on: ['--include-methods'], off: ['--no-include-methods'] },
168
+ text: 'Include method callees where receiver evidence permits. Caller-bearing views always tier method sites.',
169
+ },
170
+ expandUnverified: { type: 'boolean', text: 'trace callers: follow unverified edges; downstream nodes remain marked possible, never confirmed.' },
171
+ minConfidence: {
172
+ type: 'number',
173
+ min: 0,
174
+ max: 1,
175
+ defaults: { '*': 0 },
176
+ text: 'Minimum ordinal evidence weight (legacy name; not a probability) for caller/callee edges, from {min} to {max} (omitted: {default}).',
177
+ },
178
+ showConfidence: {
179
+ type: 'boolean',
180
+ cli: { on: ['--show-confidence'], off: ['--hide-confidence', '--no-confidence'] },
181
+ defaults: { '*': true },
182
+ text: 'show: resolution-evidence labels default to visible (omitted: {default}); set false to hide them. Numeric weights are ordinal, not probabilities.',
183
+ },
184
+ unreachableOnly: { type: 'boolean', text: 'show/impact: retain only relationships unreachable from detected entry points.' },
185
+ withTypes: { type: 'boolean', text: 'show: include related type definitions.' },
186
+ withSource: { type: 'boolean', text: 'Attach exact source to find results.' },
187
+ detailed: { type: 'boolean', text: 'repo files: show symbols per file; deps: include import declarations and importers.' },
188
+ exact: { type: 'boolean', text: 'Exact name match only (no substring matching)' },
189
+ in: { type: 'string', text: 'Only search in this directory path (e.g. "src/core")' },
190
+ top: {
191
+ type: 'integer',
192
+ min: 1,
193
+ max: 10000,
194
+ facts: { unverifiedRows: 10, hot: 10 },
195
+ text: 'Rows per list: a positive integer up to {max}. Omitted, confirmed caller and callee lists are complete, each unverified list prints {fact:unverifiedRows} rows and repo stats lists {fact:hot} hot functions. On search it bounds matches as limit does.',
196
+ },
197
+ depth: {
198
+ type: 'integer',
199
+ min: 0,
200
+ max: 100,
201
+ defaults: { trace: 3, 'trace:entrypoints': 5, deps: 2, tests: 0 },
202
+ text: 'Levels to follow: an integer from {min} to {max}. Omitted: {defaults}. tests at depth {default:tests} lists direct links only.',
203
+ },
204
+ codeOnly: { type: 'boolean', text: 'Exclude matches in comments and strings' },
205
+ context: {
206
+ type: 'integer',
207
+ min: 0,
208
+ max: 1000,
209
+ defaults: { '*': 0 },
210
+ text: 'Lines of context around each match: an integer from {min} to {max} (omitted: {default}).',
211
+ },
212
+ includeExported: { type: 'boolean', text: 'Include exported symbols in deadcode results' },
213
+ includeDecorated: { type: 'boolean', text: 'Include decorated/annotated symbols in deadcode results' },
214
+ callsOnly: { type: 'boolean', text: 'tests: retain direct calls and test-case matches only.' },
215
+ maxLines: {
216
+ type: 'integer',
217
+ min: 1,
218
+ max: 1000000,
219
+ text: 'source: maximum lines per declaration, a positive integer up to {max}; raw mode discloses any truncation.',
220
+ },
221
+ direction: {
222
+ type: 'string',
223
+ values: { trace: ['callees', 'callers'], deps: ['imports', 'importers', 'both'] },
224
+ defaults: { trace: 'callees', deps: 'both' },
225
+ text: '{valueSets}. Omitted: {defaults}.',
226
+ },
227
+ to: {
228
+ type: 'string',
229
+ values: { trace: ['entrypoints'] },
230
+ text: 'trace with direction=callers: continue toward entry points. {valueSets}.',
231
+ },
232
+ cycles: { type: 'boolean', text: 'deps: report circular imports instead of a file graph.' },
233
+ term: { type: 'string', cli: [], text: 'Literal search term by default. Set regex=true only for regular-expression syntax.' },
234
+ regex: {
235
+ type: 'boolean',
236
+ cli: { on: ['--regex'], off: ['--no-regex'] },
237
+ text: 'Treat search term as a regular expression; omitted, the term is literal. Ordinary patterns run in an RE2-compatible linear-time engine; unsafe nested repetition is rejected.',
238
+ },
239
+ functions: { type: 'boolean', text: 'repo stats: include per-function line counts sorted by size.' },
240
+ hot: { type: 'boolean', text: 'repo stats: include the top N most-called functions.' },
241
+ diverse: { type: 'boolean', text: 'show example: return representatives from distinct argument shapes.' },
242
+ lines: { type: 'boolean', text: '{commands}: path:line:text listings without default row caps; explicit top/limit still apply. show lists the sections its sections parameter names. Tier tags follow a tab; accounting and notes follow as "# " lines. Under an output budget the rows that fit are printed and one "# truncated" line counts the rest.' },
243
+ raw: { type: 'boolean', text: 'source: code text without a header or line-number gutter, including full large classes. Notes follow as "# " lines; MCP transport budgets still apply.' },
244
+ git: { type: 'boolean', text: 'show summary: attach last-modified, author, and recent-change metadata.' },
245
+ addParam: { type: 'string', text: 'Parameter name to add (plan command)' },
246
+ removeParam: { type: 'string', text: 'Parameter name to remove (plan command)' },
247
+ renameTo: { type: 'string', text: 'New name for the function, method, or type (plan command)' },
248
+ defaultValue: { type: 'string', cli: ['--default-value', '--default'], text: 'Default value for added parameter (plan command)' },
249
+ delete: { type: 'boolean', text: 'plan: assess deleting the declaration. Returns no-static-dependents, has-static-dependents or review with its basis; static evidence, never a deletion guarantee.' },
250
+ stack: { type: 'string', text: 'The stack trace text to parse (stacktrace command)' },
251
+ range: { type: 'string', text: 'source line range, e.g. "10-20" or "15", of the file given as file or name.' },
252
+ base: {
253
+ type: 'string',
254
+ defaults: { '*': 'HEAD' },
255
+ text: 'Git ref (omitted: {default}). impact/check without a symbol: diff against it; check with a symbol: compare the declaration with its version there. E.g. "HEAD~3", "main", a commit SHA',
256
+ },
257
+ staged: { type: 'boolean', text: 'impact/check without a symbol: analyze staged changes.' },
258
+ strict: { type: 'boolean', text: 'check without a symbol: REVIEW_REQUIRED fails too (trust.passes=false), not only BLOCKED.' },
259
+ deep: { type: 'boolean', text: 'repo: include health and sample the ordinal resolution-evidence profile, not accuracy.' },
260
+ compact: {
261
+ type: 'boolean',
262
+ cli: { on: ['--compact'], off: ['--no-compact'] },
263
+ defaults: { show: true, impact: true, find: false, usages: false },
264
+ text: 'Compact output. Omitted: {defaults}; set the opposite value to change that command\'s presentation.',
265
+ },
266
+ caseSensitive: { type: 'boolean', text: 'Case-sensitive search; omitted, the search ignores case.' },
267
+ all: { type: 'boolean', text: 'Lift formatter and result caps where supported.' },
268
+ topLevel: { type: 'boolean', text: 'repo files: show only top-level functions.' },
269
+ className: { type: 'string', text: 'Class name to scope method analysis (e.g. "MarketDataFetcher" for close)' },
270
+ line: {
271
+ type: 'integer',
272
+ min: 1,
273
+ max: Number.MAX_SAFE_INTEGER,
274
+ text: 'Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.',
275
+ },
276
+ limit: {
277
+ type: 'integer',
278
+ min: 1,
279
+ max: 1000000,
280
+ defaults: { find: 500, search: 500, 'search:structural': 50, api: 500, repo: 500, deadcode: 500 },
281
+ facts: { findDetail: 5 },
282
+ text: 'Max results: a positive integer up to {max}. Omitted: {defaults}; other commands and lines listings have no cap. For repo it bounds the files section. find text shows the top {fact:findDetail} in detail and counts the rest; limit raises both.',
283
+ },
284
+ maxFiles: {
285
+ type: 'integer',
286
+ min: 1,
287
+ max: 10000000,
288
+ global: true,
289
+ defaults: { '*': 50000 },
290
+ text: 'Max files to index: a positive integer up to {max} (omitted: {default}). Use for very large codebases.',
291
+ },
292
+ maxChars: {
293
+ type: 'integer',
294
+ min: 1,
295
+ max: 100000,
296
+ global: true,
297
+ defaults: {
298
+ ...Object.fromEntries(CANONICAL_COMMANDS.filter(command => BROAD_COMMANDS.has(command))
299
+ .map(command => [command, BROAD_OUTPUT_CHARS])),
300
+ '*': 10000,
301
+ },
302
+ text: 'Max output characters, not UTF-8 bytes, including notes and preserved metadata: a positive integer up to {max}. Omitted: {defaults}. all=true lifts formatter caps but keeps the {max} transport ceiling.',
303
+ },
304
+ type: {
305
+ type: 'string',
306
+ values: {
307
+ search: SEARCH_TYPES,
308
+ find: FIND_TYPES,
309
+ entrypoints: () => require('./entrypoints').ENTRYPOINT_TYPES,
310
+ },
311
+ text: 'Kind filter. {valueSets}. On search it selects structural, index-based search.',
312
+ },
313
+ param: { type: 'string', text: 'Filter by parameter name or type (structural search). E.g. "Request", "ctx".' },
314
+ receiver: { type: 'string', text: 'Filter calls by receiver (structural search, type=call). E.g. "db", "http".' },
315
+ returns: { type: 'string', text: 'Filter by return type (structural search). E.g. "Promise", "error".' },
316
+ decorator: { type: 'string', text: 'Filter by decorator/annotation (structural search). E.g. "Route", "Test".' },
317
+ exported: { type: 'boolean', text: 'Only exported/public symbols (structural search).' },
318
+ unused: { type: 'boolean', text: 'Only symbols with zero callers (structural search).' },
319
+ framework: {
320
+ type: 'string',
321
+ list: true,
322
+ fold: 'lower',
323
+ // entrypoints takes the labels of its patterns. endpoints takes a
324
+ // label its detectors declare or one a row of the answer carries,
325
+ // which its handler decides. The sets are long and every row prints
326
+ // its label: the description names examples, a refusal lists the set.
327
+ values: { entrypoints: () => require('./entrypoints').ENTRYPOINT_FRAMEWORKS },
328
+ examples: ['express', 'spring', 'flask'],
329
+ text: 'Framework label filter for entrypoints (comma-separated for several) and endpoints, e.g. {examples}. An unknown label is refused with the labels the command knows.',
330
+ },
331
+ followSymlinks: {
332
+ type: 'boolean',
333
+ global: true,
334
+ cli: { on: [], off: ['--no-follow-symlinks'] },
335
+ defaults: { '*': true },
336
+ text: 'Follow symlinks during file discovery (omitted: {default}).',
337
+ },
338
+ includeBundled: {
339
+ type: 'boolean',
340
+ global: true,
341
+ text: 'Index *.min.js and *.bundle.js files, which are left out when omitted. Source maps remain disclosed but unindexed. Bypasses the shared cache.',
342
+ },
343
+ bridge: { type: 'boolean', text: 'Match server routes to client requests (endpoints command).' },
344
+ serverOnly: { type: 'boolean', text: 'Only list server routes (endpoints command).' },
345
+ clientOnly: { type: 'boolean', text: 'Only list client requests (endpoints command).' },
346
+ unmatched: { type: 'boolean', text: 'Only show unmatched routes/requests (endpoints command).' },
347
+ method: {
348
+ type: 'string',
349
+ fold: 'upper',
350
+ values: { endpoints: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'ALL', 'USE'] },
351
+ text: 'Filter by HTTP method, in any letter case. {valueSets}.',
352
+ },
353
+ prefix: { type: 'string', text: 'Filter routes/requests by path prefix (endpoints command).' },
354
+ hideUncertain: {
355
+ type: 'boolean',
356
+ cli: { on: ['--hide-uncertain', '--no-uncertain'], off: [] },
357
+ text: 'Hide uncertain (interpolated-path) bridges (endpoints command).',
358
+ },
124
359
  };
125
360
 
361
+ const toSnakeCase = name => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
362
+ const toKebabCase = name => name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
363
+
364
+ // ============================================================================
365
+ // PARAM NORMALIZATION (snake_case → camelCase)
366
+ // ============================================================================
367
+
368
+ // MCP spells a parameter in snake_case. Derived from the table; `project_dir`
369
+ // is the one transport field that is not a parameter of a command.
370
+ const PARAM_MAP = { project_dir: 'projectDir' };
371
+ for (const param of Object.keys(PARAMS)) {
372
+ const snake = toSnakeCase(param);
373
+ if (snake !== param) PARAM_MAP[snake] = param;
374
+ }
375
+
126
376
  // ============================================================================
127
377
  // FLAG APPLICABILITY MATRIX
128
378
  // ============================================================================
@@ -144,8 +394,8 @@ const FLAG_APPLICABILITY = {
144
394
  tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
145
395
  deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
146
396
  api: ['file', 'in', 'limit', 'includeTests'],
147
- check: ['name', 'file', 'className', 'line', 'includeMethods', 'base', 'staged', 'limit'],
148
- plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
397
+ check: ['name', 'file', 'className', 'line', 'includeMethods', 'base', 'staged', 'limit', 'strict'],
398
+ plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue', 'delete'],
149
399
  repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
150
400
  deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
151
401
  entrypoints: ['file', 'exclude', 'includeTests', 'excludeTests', 'limit', 'type', 'framework'],
@@ -154,19 +404,265 @@ const FLAG_APPLICABILITY = {
154
404
  auditAsync: ['file', 'exclude', 'limit'],
155
405
  };
156
406
 
157
- // Project/change-wide sweep commands with no required symbol argument —
158
- // truncation means you need a filter, not more text. Drives the shared
159
- // CLI/MCP output budget: broad commands default to 3K chars, everything
160
- // else (symbol-targeted commands like usages/tests included) to 10K.
161
- const BROAD_COMMANDS = new Set([
162
- 'repo', 'entrypoints', 'endpoints', 'deadcode',
163
- 'deps', 'check', 'auditAsync',
164
- ]);
165
-
166
407
  // Commands that can operate on a single file without a project index.
167
408
  // Used by CLI to decide whether to build a file-local or project-wide index.
168
409
  const FILE_LOCAL_COMMANDS = new Set(['find', 'usages', 'search', 'source', 'api']);
169
410
 
411
+ // Parameters whose presence selects structural (index-based) search.
412
+ const STRUCTURAL_SEARCH_PARAMS = Object.freeze(['type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused']);
413
+
414
+ // ============================================================================
415
+ // PARAMETER TABLE ACCESS
416
+ // ============================================================================
417
+
418
+ const GLOBAL_PARAMS = Object.freeze(Object.keys(PARAMS).filter(param => PARAMS[param].global));
419
+ const BOOLEAN_PARAMS = Object.freeze(Object.keys(PARAMS).filter(param => PARAMS[param].type === 'boolean'));
420
+
421
+ /**
422
+ * The mode of a command that changes a value set or a default: `show` with
423
+ * lines, `repo` with deep, `trace` toward entry points, structural `search`.
424
+ */
425
+ function commandMode(command, params = {}) {
426
+ if (command === 'show' && params.lines) return 'lines';
427
+ if (command === 'repo' && params.deep) return 'deep';
428
+ if (command === 'trace' && params.to === 'entrypoints') return 'entrypoints';
429
+ if (command === 'search' && STRUCTURAL_SEARCH_PARAMS.some(key => params[key])) return 'structural';
430
+ return null;
431
+ }
432
+
433
+ /** The entry of a per-command map for `command` in `mode`: `command:mode`, then `command`, then '*'. */
434
+ function commandEntry(map, command, mode) {
435
+ if (!map) return undefined;
436
+ if (mode && Object.hasOwn(map, `${command}:${mode}`)) return map[`${command}:${mode}`];
437
+ if (Object.hasOwn(map, command)) return map[command];
438
+ return map['*'];
439
+ }
440
+
441
+ const resolvedValueSets = new Map();
442
+ /** A value set, calling a deferred one once. */
443
+ function resolveValueSet(param, key, set) {
444
+ if (typeof set !== 'function') return set;
445
+ const cacheKey = `${param}\u0000${key}`;
446
+ if (!resolvedValueSets.has(cacheKey)) resolvedValueSets.set(cacheKey, Object.freeze([...set()]));
447
+ return resolvedValueSets.get(cacheKey);
448
+ }
449
+
450
+ /**
451
+ * The closed set of values `param` accepts on `command`, or null when the
452
+ * parameter is free-form there. `mode` selects a narrower set where the
453
+ * table has one (`show` with lines).
454
+ */
455
+ function paramValues(param, command, mode = null) {
456
+ const values = PARAMS[param]?.values;
457
+ if (!values) return null;
458
+ if (Array.isArray(values)) return values;
459
+ if (typeof values === 'function') return resolveValueSet(param, '*', values);
460
+ const key = mode && Object.hasOwn(values, `${command}:${mode}`) ? `${command}:${mode}`
461
+ : Object.hasOwn(values, command) ? command
462
+ : Object.hasOwn(values, '*') ? '*' : null;
463
+ return key === null ? null : resolveValueSet(param, key, values[key]);
464
+ }
465
+
466
+ /** What the engine uses when `param` is omitted on `command` (in `mode`); undefined when it has no default there. */
467
+ function paramDefault(param, command = '*', mode = null) {
468
+ return commandEntry(PARAMS[param]?.defaults, command, mode);
469
+ }
470
+
471
+ /** A number the description of `param` states. */
472
+ function paramFact(param, name) {
473
+ return PARAMS[param]?.facts?.[name];
474
+ }
475
+
476
+ /** Inclusive bounds of a numeric parameter. */
477
+ function paramRange(param) {
478
+ const spec = PARAMS[param] || {};
479
+ return { min: spec.min, max: spec.max };
480
+ }
481
+
482
+ function integerLabel(min) {
483
+ return min === 0 ? 'non-negative integer' : 'positive integer';
484
+ }
485
+
486
+ /**
487
+ * Why `value` is outside the bounds of numeric `param`, or null. `label` is
488
+ * the spelling the message uses for the parameter.
489
+ */
490
+ function paramRangeError(param, value, label = param) {
491
+ const spec = PARAMS[param];
492
+ if (!spec || (spec.type !== 'integer' && spec.type !== 'number')) return null;
493
+ const n = typeof value === 'number' ? value : Number(String(value).trim());
494
+ if (spec.type === 'integer') {
495
+ if (String(value).trim() === '' || !Number.isInteger(n) || n < spec.min) {
496
+ return `Invalid ${label} value: must be a ${integerLabel(spec.min)} (got ${typeof value === 'number' ? value : `"${value}"`})`;
497
+ }
498
+ if (n > spec.max) return `Invalid ${label} value: ${n} exceeds maximum (${spec.max})`;
499
+ return null;
500
+ }
501
+ if (String(value).trim() === '' || !Number.isFinite(n) || n < spec.min || n > spec.max) {
502
+ return `Invalid ${label} value: "${value}" — must be a number between ${spec.min} and ${spec.max}.`;
503
+ }
504
+ return null;
505
+ }
506
+
507
+ const foldValue = (fold, value) => (fold === 'upper' ? value.toUpperCase() : fold === 'lower' ? value.toLowerCase() : value);
508
+
509
+ /** The values a caller wrote for a closed-set parameter: each as written and as the table folds it. */
510
+ function writtenValues(param, value) {
511
+ const spec = PARAMS[param];
512
+ // Scalar enums are exact strings in the MCP schema. Validate the value
513
+ // dispatch will actually receive; trimming here silently selected a
514
+ // different mode when its handler compared the original spelling.
515
+ if (!spec.list && !spec.fold) return [{ written: String(value), folded: String(value) }];
516
+ const parts = spec.list
517
+ ? (Array.isArray(value) ? value : String(value).split(','))
518
+ : [value];
519
+ return parts.map(part => String(part).trim()).filter(Boolean)
520
+ .map(written => ({ written, folded: foldValue(spec.fold, written) }));
521
+ }
522
+
523
+ /**
524
+ * Why a parameter of `command` holds a value the table does not allow, or
525
+ * null: a number outside its bounds, a value outside a closed set. One rule
526
+ * and one wording for every surface and every programmatic caller.
527
+ */
528
+ function paramTableError(command, params = {}) {
529
+ const applicable = FLAG_APPLICABILITY[command] || [];
530
+ const mode = commandMode(command, params);
531
+ for (const [param, spec] of Object.entries(PARAMS)) {
532
+ const value = params[param];
533
+ if (value === undefined || value === null) continue;
534
+ if (!spec.global && !applicable.includes(param)) continue;
535
+ const rangeError = paramRangeError(param, value);
536
+ if (rangeError) return rangeError;
537
+ const allowed = paramValues(param, command, mode);
538
+ if (!allowed || (spec.list && (value === '' || (Array.isArray(value) && value.length === 0)))) continue;
539
+ const subject = `${toMcpName(command)}${mode === 'lines' ? ' lines' : ''} ${param}`;
540
+ if (!spec.list && typeof value !== 'string') return `Invalid ${subject}: must be a string. Valid: ${allowed.join(', ')}.`;
541
+ const written = writtenValues(param, value);
542
+ if (written.length === 0) return `No ${subject} given. Valid: ${allowed.join(', ')}.`;
543
+ const unknown = written.filter(part => !allowed.includes(part.folded)).map(part => part.written);
544
+ if (unknown.length > 0) {
545
+ return `Invalid ${subject} "${unknown.join(', ')}". Valid: ${allowed.join(', ')}.`;
546
+ }
547
+ }
548
+ return null;
549
+ }
550
+
551
+ // How each surface names a command mode.
552
+ const MODE_LABELS = Object.freeze({
553
+ mcp: {
554
+ 'show:lines': 'show with lines',
555
+ 'repo:deep': 'repo with deep',
556
+ 'trace:entrypoints': 'trace with to=entrypoints',
557
+ 'search:structural': 'structural search',
558
+ },
559
+ cli: {
560
+ 'show:lines': 'show --lines',
561
+ 'repo:deep': 'repo --deep',
562
+ 'trace:entrypoints': 'trace --to=entrypoints',
563
+ 'search:structural': 'structural search',
564
+ },
565
+ });
566
+
567
+ function tableKeyLabel(key, surface) {
568
+ if (key === '*') return 'other commands';
569
+ if (MODE_LABELS[surface][key]) return MODE_LABELS[surface][key];
570
+ return surface === 'mcp' ? toMcpName(key) : toCliName(key);
571
+ }
572
+
573
+ /** Every value set of a parameter as `command: values`, one entry per command or mode. */
574
+ function valueSetsText(param, surface = 'mcp') {
575
+ const spec = PARAMS[param];
576
+ const join = values => values.join(spec.list ? ',' : ', ');
577
+ if (Array.isArray(spec.values) || typeof spec.values === 'function') return [join(paramValues(param, '*'))];
578
+ return Object.keys(spec.values).map(key => {
579
+ const [command, mode] = key.split(':');
580
+ return `${tableKeyLabel(key, surface)}: ${join(paramValues(param, command, mode || null))}`;
581
+ });
582
+ }
583
+
584
+ /**
585
+ * Every default of a parameter as `commands value`: commands that share a
586
+ * value are named together, in the table's order. A lone '*' entry is the
587
+ * bare value.
588
+ */
589
+ function defaultsText(param, surface = 'mcp') {
590
+ const spec = PARAMS[param];
591
+ const text = value => (Array.isArray(value) ? value.join(',') : String(value));
592
+ const entries = Object.entries(spec.defaults);
593
+ if (entries.length === 1 && entries[0][0] === '*') return [text(entries[0][1])];
594
+ const groups = new Map();
595
+ for (const [key, value] of entries) {
596
+ const rendered = text(value);
597
+ if (!groups.has(rendered)) groups.set(rendered, []);
598
+ groups.get(rendered).push(tableKeyLabel(key, surface));
599
+ }
600
+ return [...groups].map(([rendered, keys]) => `${keys.join(', ')} ${rendered}`);
601
+ }
602
+
603
+ /** The description of a parameter with every {placeholder} filled from the table. */
604
+ function describeParam(param) {
605
+ const spec = PARAMS[param];
606
+ const join = values => (Array.isArray(values) ? values.join(spec.list ? ',' : ', ') : String(values));
607
+ return spec.text.replace(/\{([a-z]+)(?::([^}]+))?\}/gi, (whole, kind, key) => {
608
+ if (kind === 'values') {
609
+ const [command, mode] = String(key || '*').split(':');
610
+ return join(paramValues(param, command, mode || null));
611
+ }
612
+ if (kind === 'default') {
613
+ const [command, mode] = String(key || '*').split(':');
614
+ return join(paramDefault(param, command, mode || null));
615
+ }
616
+ if (kind === 'valueSets') return valueSetsText(param).join('. ');
617
+ if (kind === 'defaults') return defaultsText(param).join('; ');
618
+ if (kind === 'min') return String(spec.min);
619
+ if (kind === 'max') return String(spec.max);
620
+ if (kind === 'fact') return String(spec.facts[key]);
621
+ if (kind === 'examples') return spec.examples.map(example => `"${example}"`).join(', ');
622
+ if (kind === 'commands') {
623
+ return CANONICAL_COMMANDS.filter(command => FLAG_APPLICABILITY[command].includes(param))
624
+ .map(toMcpName).join('/');
625
+ }
626
+ throw new Error(`registry: unknown placeholder ${whole} in the description of ${param}`);
627
+ });
628
+ }
629
+
630
+ /**
631
+ * The values a JSON schema can enumerate for `param`: the union over
632
+ * commands, when every value is written exactly as listed. A list-valued or
633
+ * case-folded parameter has no schema enumeration; its description states
634
+ * the values and the engine checks them.
635
+ */
636
+ function paramSchemaEnum(param) {
637
+ const spec = PARAMS[param];
638
+ if (!spec.values || spec.list || spec.fold) return null;
639
+ if (Array.isArray(spec.values) || typeof spec.values === 'function') return [...paramValues(param, '*')];
640
+ const union = [];
641
+ for (const key of Object.keys(spec.values)) {
642
+ for (const value of resolveValueSet(param, key, spec.values[key])) {
643
+ if (!union.includes(value)) union.push(value);
644
+ }
645
+ }
646
+ return union;
647
+ }
648
+
649
+ /** The MCP input schema property of every parameter, keyed by its snake_case name. */
650
+ function mcpParamProperties() {
651
+ const properties = {};
652
+ for (const [param, spec] of Object.entries(PARAMS)) {
653
+ const rule = { type: spec.type, description: describeParam(param) };
654
+ if (spec.type === 'integer' || spec.type === 'number') {
655
+ if (spec.type === 'integer' && spec.min === 1) rule.exclusiveMinimum = 0;
656
+ else if (spec.min !== undefined) rule.minimum = spec.min;
657
+ if (spec.max !== undefined) rule.maximum = spec.max;
658
+ }
659
+ const enumeration = paramSchemaEnum(param);
660
+ if (enumeration) rule.enum = enumeration;
661
+ properties[toSnakeCase(param)] = rule;
662
+ }
663
+ return properties;
664
+ }
665
+
170
666
  // ============================================================================
171
667
  // HELPERS
172
668
  // ============================================================================
@@ -301,21 +797,17 @@ function formatSurfaceMessage(message, surface = 'cli') {
301
797
  }
302
798
  }
303
799
  const knownParams = new Set(Object.values(FLAG_APPLICABILITY).flat());
304
- const booleanParams = new Set([
305
- 'includeTests', 'excludeTests', 'includeMethods', 'withTypes',
306
- 'codeOnly', 'caseSensitive', 'includeExported', 'includeDecorated',
307
- 'showConfidence', 'callsOnly', 'topLevel', 'followSymlinks', 'includeBundled',
308
- 'unreachableOnly', 'serverOnly', 'clientOnly', 'hideUncertain',
309
- 'expandUnverified', 'withSource', 'all', 'compact', 'exact',
310
- 'regex', 'exported', 'unused', 'staged', 'detailed', 'functions',
311
- 'hot', 'deep', 'cycles', 'bridge', 'unmatched', 'diverse', 'git',
312
- 'raw', 'lines',
313
- ]);
800
+ const booleanParams = new Set(BOOLEAN_PARAMS);
314
801
  if (surface === 'mcp') {
802
+ rendered = rendered.replace(/--([a-z][a-z0-9-]*)[ \t]+N\b/g, (whole, flag) => {
803
+ const camel = flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
804
+ return ['integer', 'number'].includes(PARAMS[camel]?.type)
805
+ ? `${REVERSE_PARAM_MAP[camel] || camel}=<n>` : whole;
806
+ });
315
807
  rendered = rendered.replace(/--([a-z][a-z0-9-]*)(?:=([^\s,.)]+))?/g,
316
808
  (whole, flag, rawValue) => {
317
809
  const camel = flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
318
- if (!knownParams.has(camel) && !['maxChars', 'maxFiles', 'includeBundled'].includes(camel)) {
810
+ if (!knownParams.has(camel) && !GLOBAL_PARAMS.includes(camel)) {
319
811
  return whole;
320
812
  }
321
813
  const snake = REVERSE_PARAM_MAP[camel] || camel;
@@ -362,36 +854,50 @@ function buildReverseParamMap() {
362
854
 
363
855
  const REVERSE_PARAM_MAP = buildReverseParamMap();
364
856
 
365
- const CLI_GLOBAL_FLAGS = Object.freeze([
857
+ /** The CLI spellings of a parameter: { on, off } for a boolean, { value } otherwise. */
858
+ function cliSpellings(param) {
859
+ const spec = PARAMS[param];
860
+ const plain = `--${toKebabCase(param)}`;
861
+ if (spec && spec.type === 'boolean') {
862
+ return { on: spec.cli ? spec.cli.on : [plain], off: spec.cli ? spec.cli.off : [] };
863
+ }
864
+ return { value: spec && spec.cli ? spec.cli : [plain] };
865
+ }
866
+
867
+ /** Every CLI flag that sets a parameter, the affirmative spellings first. */
868
+ function cliFlagsForParam(param) {
869
+ const spellings = cliSpellings(param);
870
+ return spellings.value || [...spellings.on, ...spellings.off];
871
+ }
872
+
873
+ // Flags of the CLI process itself: none is a parameter of a command.
874
+ const CLI_PROCESS_FLAGS = Object.freeze([
366
875
  '--help', '-h', '--version', '-v', '--mcp',
367
876
  '--json', '--verbose', '--no-quiet', '--quiet',
368
877
  '--interactive', '-i',
369
- '--no-cache', '--clear-cache', '--no-follow-symlinks', '--include-bundled',
370
- '--max-files', '--max-chars', '--workers',
878
+ '--no-cache', '--clear-cache', '--workers',
371
879
  ]);
880
+ // Process flags that take a value.
881
+ const CLI_PROCESS_VALUE_FLAGS = Object.freeze(['--workers']);
372
882
 
373
- const CLI_PARAM_FLAG_OVERRIDES = Object.freeze({
374
- exclude: ['--exclude', '--not'],
375
- includeMethods: ['--include-methods', '--no-include-methods'],
376
- regex: ['--regex', '--no-regex'],
377
- showConfidence: ['--show-confidence', '--hide-confidence', '--no-confidence'],
378
- hideUncertain: ['--hide-uncertain', '--no-uncertain'],
379
- compact: ['--compact', '--no-compact'],
380
- defaultValue: ['--default-value', '--default'],
381
- });
883
+ const CLI_GLOBAL_FLAGS = Object.freeze([
884
+ ...CLI_PROCESS_FLAGS,
885
+ ...GLOBAL_PARAMS.flatMap(cliFlagsForParam),
886
+ ]);
382
887
 
383
- function cliFlagsForParam(param) {
384
- if (CLI_PARAM_FLAG_OVERRIDES[param]) {
385
- return CLI_PARAM_FLAG_OVERRIDES[param];
888
+ /** Every CLI flag that consumes a value (`--flag value` or `--flag=value`). */
889
+ function getCliValueFlags() {
890
+ const flags = new Set(CLI_PROCESS_VALUE_FLAGS);
891
+ for (const param of Object.keys(PARAMS)) {
892
+ const spellings = cliSpellings(param);
893
+ if (spellings.value) for (const flag of spellings.value) flags.add(flag);
386
894
  }
387
- const snake = REVERSE_PARAM_MAP[param] || param;
388
- return [`--${snake.replace(/_/g, '-')}`];
895
+ return flags;
389
896
  }
390
897
 
391
898
  function getCliFlagsForCommand(command) {
392
899
  const canonical = resolveCommand(command, 'cli') || command;
393
900
  return [...new Set((FLAG_APPLICABILITY[canonical] || [])
394
- .filter(param => !['name', 'term'].includes(param))
395
901
  .flatMap(cliFlagsForParam))];
396
902
  }
397
903
 
@@ -403,13 +909,38 @@ function getCliAcceptedFlags() {
403
909
  return accepted;
404
910
  }
405
911
 
912
+ /**
913
+ * The value lists, bounds and defaults of the CLI help: one entry per
914
+ * parameter that has any, written from the table.
915
+ */
916
+ function generateCliParamReference() {
917
+ const lines = [];
918
+ for (const [param, spec] of Object.entries(PARAMS)) {
919
+ const parts = [];
920
+ if (spec.type === 'integer' || spec.type === 'number') {
921
+ parts.push(spec.max >= Number.MAX_SAFE_INTEGER ? `${spec.min} or more` : `${spec.min} to ${spec.max}`);
922
+ }
923
+ if (spec.examples) {
924
+ parts.push(`e.g. ${spec.examples.join(', ')}; an unknown value is refused with the known ones`);
925
+ } else if (spec.values) {
926
+ parts.push(...valueSetsText(param, 'cli'));
927
+ }
928
+ if (spec.defaults) parts.push(`default: ${defaultsText(param, 'cli').join('; ')}`);
929
+ if (parts.length === 0) continue;
930
+ const head = ` ${cliFlagsForParam(param)[0]}`.padEnd(19);
931
+ lines.push(`${head}${parts[0]}`);
932
+ for (const part of parts.slice(1)) lines.push(`${' '.repeat(19)}${part}`);
933
+ }
934
+ return lines.join('\n');
935
+ }
936
+
406
937
  /**
407
938
  * Generate per-command parameter listing for the MCP tool description.
408
939
  * Maps camelCase flags back to snake_case for MCP clients.
409
940
  * One line per command: `show: file, exclude, class_name, ...`
410
941
  */
411
942
  function generateMcpParamSection() {
412
- const lines = ['', 'ACCEPTED FLAGS PER COMMAND (max_chars, max_files, follow_symlinks, include_bundled always accepted; flags not listed below are ignored):'];
943
+ const lines = ['', `ACCEPTED FLAGS PER COMMAND (${GLOBAL_PARAMS.map(toSnakeCase).sort().join(', ')} always accepted; flags not listed below are ignored):`];
413
944
  for (const cmd of CANONICAL_COMMANDS) {
414
945
  const flags = FLAG_APPLICABILITY[cmd];
415
946
  if (!flags || flags.length === 0) continue;
@@ -438,7 +969,27 @@ module.exports = {
438
969
  toCliName,
439
970
  formatSurfaceMessage,
440
971
  cliFlagsForParam,
972
+ cliSpellings,
441
973
  getCliFlagsForCommand,
442
974
  getCliAcceptedFlags,
975
+ getCliValueFlags,
443
976
  generateMcpParamSection,
977
+ generateCliParamReference,
978
+ PARAMS,
979
+ GLOBAL_PARAMS,
980
+ BOOLEAN_PARAMS,
981
+ STRUCTURAL_SEARCH_PARAMS,
982
+ CLI_PROCESS_FLAGS,
983
+ commandMode,
984
+ paramValues,
985
+ paramDefault,
986
+ paramFact,
987
+ paramRange,
988
+ paramRangeError,
989
+ paramTableError,
990
+ describeParam,
991
+ paramSchemaEnum,
992
+ mcpParamProperties,
993
+ toSnakeCase,
994
+ toKebabCase,
444
995
  };