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.
- package/.claude/skills/ucn/SKILL.md +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/registry.js
CHANGED
|
@@ -88,41 +88,291 @@ function v4MigrationHint(name, surface = 'cli') {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// ============================================================================
|
|
91
|
-
//
|
|
91
|
+
// PARAMETER TABLE
|
|
92
92
|
// ============================================================================
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 the requested view. find includes test definitions by default; set false to omit them.' },
|
|
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) && !
|
|
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
|
-
|
|
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', '--
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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 = ['',
|
|
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
|
};
|