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.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/execute.js
CHANGED
|
@@ -15,11 +15,18 @@ const path = require('path');
|
|
|
15
15
|
const { addTestExclusions, pickBestDefinition, parseSymbolHandle, looksLikeHandle } = require('./shared');
|
|
16
16
|
const { cleanHtmlScriptTags, detectLanguage } = require('./parser');
|
|
17
17
|
const { renderExpandItem } = require('./expand-cache');
|
|
18
|
+
const { CANONICAL_COMMANDS } = require('./registry');
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// HELPERS
|
|
21
22
|
// ============================================================================
|
|
22
23
|
|
|
24
|
+
const NO_STATIC_TEST_LINK_NOTE =
|
|
25
|
+
'No confirmed static test link was found. This is not runtime coverage evidence: ' +
|
|
26
|
+
'subprocess/black-box tests, reflection, generated code, and external harnesses can still ' +
|
|
27
|
+
'exercise the target. Inspect the project test runner and search for the defining module/file ' +
|
|
28
|
+
'before concluding it is untested.';
|
|
29
|
+
|
|
23
30
|
function requireName(name) {
|
|
24
31
|
if (!name || (typeof name === 'string' && !name.trim())) {
|
|
25
32
|
return 'Symbol name is required.';
|
|
@@ -41,6 +48,59 @@ function requireTerm(term) {
|
|
|
41
48
|
return null;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
function editDistance(a, b) {
|
|
52
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
53
|
+
for (let i = 1; i <= a.length; i++) {
|
|
54
|
+
let diagonal = prev[0];
|
|
55
|
+
prev[0] = i;
|
|
56
|
+
for (let j = 1; j <= b.length; j++) {
|
|
57
|
+
const above = prev[j];
|
|
58
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1,
|
|
59
|
+
diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
60
|
+
diagonal = above;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return prev[b.length];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function symbolNotFound(index, name, label = 'Symbol') {
|
|
67
|
+
const query = String(name || '');
|
|
68
|
+
const lower = query.toLowerCase();
|
|
69
|
+
let best = null;
|
|
70
|
+
for (const candidate of index.symbols.keys()) {
|
|
71
|
+
const value = String(candidate);
|
|
72
|
+
const candidateLower = value.toLowerCase();
|
|
73
|
+
const distance = editDistance(lower, candidateLower);
|
|
74
|
+
const prefixBonus = candidateLower.startsWith(lower) || lower.startsWith(candidateLower)
|
|
75
|
+
? -2 : 0;
|
|
76
|
+
const score = distance + prefixBonus;
|
|
77
|
+
if (!best || score < best.score ||
|
|
78
|
+
(score === best.score && value < best.value)) {
|
|
79
|
+
best = { value, score, distance };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const threshold = Math.max(2, Math.floor(query.length * 0.3));
|
|
83
|
+
let message;
|
|
84
|
+
if (best && best.distance <= threshold) {
|
|
85
|
+
message = `${label} "${query}" not found. Did you mean "${best.value}"? ` +
|
|
86
|
+
'Use find to confirm the symbol and obtain a stable handle.';
|
|
87
|
+
} else {
|
|
88
|
+
message = `${label} "${query}" not found. Use find with a shorter name, or search for a literal occurrence.`;
|
|
89
|
+
}
|
|
90
|
+
const unsupported = require('./account').scanUnsupportedFiles(index, query);
|
|
91
|
+
if (unsupported.lines > 0) {
|
|
92
|
+
const languages = Object.keys(unsupported.languages).join(', ');
|
|
93
|
+
message += ` ${unsupported.lines} literal match(es) exist in ` +
|
|
94
|
+
`${unsupported.fileCount} unsupported-language file(s)` +
|
|
95
|
+
`${languages ? ` (${languages})` : ''}; this is not a semantic zero. ` +
|
|
96
|
+
'Use grep/ripgrep plus a language-native analyzer.';
|
|
97
|
+
} else if (index.unsupportedFiles?.length > 0) {
|
|
98
|
+
message += ` UCN skipped ${index.unsupportedFiles.length} unsupported source ` +
|
|
99
|
+
`file(s), so this is not a repository-wide semantic zero; verify with grep/ripgrep.`;
|
|
100
|
+
}
|
|
101
|
+
return message;
|
|
102
|
+
}
|
|
103
|
+
|
|
44
104
|
/**
|
|
45
105
|
* Split Class.method syntax into className and methodName.
|
|
46
106
|
* Returns { className, methodName } or null if not applicable.
|
|
@@ -53,10 +113,21 @@ function splitClassMethod(name) {
|
|
|
53
113
|
if (dotIndex <= 0 || dotIndex === name.length - 1) return null;
|
|
54
114
|
// Only split on first dot, and only if there's exactly one dot
|
|
55
115
|
if (name.indexOf('.', dotIndex + 1) !== -1) return null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
const className = name.substring(0, dotIndex);
|
|
117
|
+
const methodName = name.substring(dotIndex + 1);
|
|
118
|
+
// This is authored identifier syntax, not a generic string splitter.
|
|
119
|
+
// Preserve computed names, malformed handles, paths, and source text.
|
|
120
|
+
const identifier = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
|
|
121
|
+
if (!identifier.test(className) || !identifier.test(methodName)) return null;
|
|
122
|
+
return { className, methodName };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateInFilter(index, value) {
|
|
126
|
+
if (!value) return null;
|
|
127
|
+
for (const [, fileEntry] of index.files) {
|
|
128
|
+
if (index.matchesFilters(fileEntry.relativePath, { in: value })) return null;
|
|
129
|
+
}
|
|
130
|
+
return `No files matched the 'in' directory filter '${value}'.`;
|
|
60
131
|
}
|
|
61
132
|
|
|
62
133
|
/**
|
|
@@ -88,12 +159,17 @@ function applyHandleSyntax(p) {
|
|
|
88
159
|
if (!looksLikeHandle(p.name)) return;
|
|
89
160
|
const h = parseSymbolHandle(p.name);
|
|
90
161
|
if (!h) return;
|
|
162
|
+
p._handleFile = h.file;
|
|
163
|
+
p._handleLine = h.line;
|
|
91
164
|
// Pull name out of handle. If the handle has no name suffix, we need to
|
|
92
165
|
// recover it from the index — but at this layer we only have params.
|
|
93
166
|
// The downstream resolveSymbol path will look up by file+line if name is empty.
|
|
94
167
|
if (h.name) p.name = h.name;
|
|
95
168
|
// Only override p.file/p.line if they weren't explicitly set by the user
|
|
96
|
-
if (h.file && !p.file)
|
|
169
|
+
if (h.file && !p.file) {
|
|
170
|
+
p.file = h.file;
|
|
171
|
+
p._fileFromHandle = true;
|
|
172
|
+
}
|
|
97
173
|
if (h.line && !p.line) p.line = h.line;
|
|
98
174
|
}
|
|
99
175
|
|
|
@@ -151,9 +227,21 @@ function disambiguationHint(matches, chosen, fileGiven) {
|
|
|
151
227
|
}
|
|
152
228
|
|
|
153
229
|
/** Check if a file-based result has a file error. */
|
|
154
|
-
function checkFileError(result, file) {
|
|
230
|
+
function checkFileError(result, file, index = null) {
|
|
155
231
|
if (!result) return null;
|
|
156
232
|
if (result.error === 'file-not-found') {
|
|
233
|
+
if (index) {
|
|
234
|
+
const exactUnsupported = (index.unsupportedFiles || []).find(entry =>
|
|
235
|
+
entry.relativePath === file || entry.relativePath.endsWith('/' + file));
|
|
236
|
+
const candidate = path.isAbsolute(file)
|
|
237
|
+
? path.resolve(file) : path.resolve(index.root, file);
|
|
238
|
+
if (exactUnsupported || fs.existsSync(candidate)) {
|
|
239
|
+
const language = exactUnsupported?.language ||
|
|
240
|
+
exactUnsupported?.extension || 'unsupported/excluded';
|
|
241
|
+
return `File exists but is not indexed by UCN: ${file} (${language}). ` +
|
|
242
|
+
'Use grep/ripgrep plus a language-native analyzer.';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
157
245
|
return `File not found in project: ${file}`;
|
|
158
246
|
}
|
|
159
247
|
if (result.error === 'file-ambiguous') {
|
|
@@ -222,8 +310,12 @@ function treeNote(result) {
|
|
|
222
310
|
// result.warnings are NOT copied here — the tree formatters render them
|
|
223
311
|
// in the body (Note: lines under the header); copying them into the
|
|
224
312
|
// handler note printed each warning twice (fix #237).
|
|
225
|
-
|
|
226
|
-
|
|
313
|
+
const countTruncated = node => !node ? 0 :
|
|
314
|
+
(node.truncatedChildren || 0) +
|
|
315
|
+
(node.children || []).reduce((sum, child) => sum + countTruncated(child), 0);
|
|
316
|
+
const truncatedChildren = countTruncated(result?.tree);
|
|
317
|
+
if (truncatedChildren > 0) {
|
|
318
|
+
parts.push(`${truncatedChildren} children truncated by the per-node cap. Use --all to expand them; --depth controls hops only.`);
|
|
227
319
|
}
|
|
228
320
|
if (result?.truncatedCallers > 0) {
|
|
229
321
|
parts.push(`${result.truncatedCallers} callers truncated. Use --all to expand.`);
|
|
@@ -284,7 +376,10 @@ function checkDefinitionPin(index, p) {
|
|
|
284
376
|
return list.length > 10 ? `${shown}\n ... and ${list.length - 10} more` : shown;
|
|
285
377
|
};
|
|
286
378
|
if (p.file) {
|
|
287
|
-
const
|
|
379
|
+
const resolvedFile = index.resolveFilePathForQuery(p.file);
|
|
380
|
+
const byFile = typeof resolvedFile === 'string'
|
|
381
|
+
? candidates.filter(d => d.file === resolvedFile)
|
|
382
|
+
: candidates.filter(d => d.relativePath && d.relativePath.includes(p.file));
|
|
288
383
|
if (byFile.length === 0) {
|
|
289
384
|
return `Symbol "${p.name}" not found in files matching "${p.file}". Found ${candidates.length} definition(s) elsewhere:\n${describe(candidates)}\nUse file= with a path fragment from the list above to disambiguate.`;
|
|
290
385
|
}
|
|
@@ -292,7 +387,8 @@ function checkDefinitionPin(index, p) {
|
|
|
292
387
|
}
|
|
293
388
|
const line = Number(p.line);
|
|
294
389
|
if (p.line && Number.isFinite(line)) {
|
|
295
|
-
const atLine = candidates.filter(d =>
|
|
390
|
+
const atLine = candidates.filter(d =>
|
|
391
|
+
d.startLine === line || d.nameLine === line);
|
|
296
392
|
if (atLine.length === 0) {
|
|
297
393
|
return `No definition of "${p.name}" at line ${line}${p.file ? ` in files matching "${p.file}"` : ''}. Definitions:\n${describe(candidates)}`;
|
|
298
394
|
}
|
|
@@ -308,12 +404,363 @@ function readAndExtract(match) {
|
|
|
308
404
|
return cleanHtmlScriptTags(extracted, detectLanguage(match.file)).join('\n');
|
|
309
405
|
}
|
|
310
406
|
|
|
407
|
+
const SHOW_SECTIONS = new Set([
|
|
408
|
+
'summary', 'callers', 'callees', 'source', 'dependencies',
|
|
409
|
+
'tests', 'types', 'example', 'related',
|
|
410
|
+
]);
|
|
411
|
+
const REPO_SECTIONS = new Set(['summary', 'files', 'stats', 'health']);
|
|
412
|
+
|
|
413
|
+
/** Parse a comma-separated/array section selector with validation. */
|
|
414
|
+
function parseSections(value, defaults, allowed, command) {
|
|
415
|
+
const supplied = value != null && value !== '';
|
|
416
|
+
const raw = !supplied
|
|
417
|
+
? defaults
|
|
418
|
+
: (Array.isArray(value) ? value : String(value).split(','));
|
|
419
|
+
const sections = [...new Set(raw.map(s => String(s).trim().toLowerCase()).filter(Boolean))];
|
|
420
|
+
if (supplied && sections.length === 0) {
|
|
421
|
+
return {
|
|
422
|
+
error: `No valid ${command} sections were provided. Available: ${[...allowed].join(', ')}.`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const invalid = sections.filter(s => !allowed.has(s));
|
|
426
|
+
if (invalid.length > 0) {
|
|
427
|
+
return {
|
|
428
|
+
error: `Unknown ${command} section(s): ${invalid.join(', ')}. Available: ${[...allowed].join(', ')}.`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { sections };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function addMode(result, mode) {
|
|
435
|
+
if (result && typeof result === 'object') {
|
|
436
|
+
Object.defineProperty(result, '_publicMode', {
|
|
437
|
+
value: mode,
|
|
438
|
+
enumerable: false,
|
|
439
|
+
configurable: true,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function combineNotes(notes) {
|
|
446
|
+
return notes.filter(Boolean).join('\n') || undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const PUBLIC_COMMAND_SET = new Set(CANONICAL_COMMANDS);
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Validate mode-dependent parameter combinations before a handler chooses a
|
|
453
|
+
* branch. This is deliberately inside the engine so CLI, MCP, interactive,
|
|
454
|
+
* glob, file, and programmatic callers receive the same answer.
|
|
455
|
+
*/
|
|
456
|
+
function validatePublicParams(command, p) {
|
|
457
|
+
const hasName = typeof p.name === 'string' && p.name.trim() !== '';
|
|
458
|
+
const gitScope = p.staged || (typeof p.base === 'string' && p.base.trim() !== '');
|
|
459
|
+
if ((command === 'impact' || command === 'check') && hasName && gitScope) {
|
|
460
|
+
return `${command} accepts either a symbol target or Git diff scope (base/staged), not both.`;
|
|
461
|
+
}
|
|
462
|
+
if ((command === 'impact' || command === 'check') && p.staged && p.base) {
|
|
463
|
+
return `${command} accepts either staged=true or base, not both.`;
|
|
464
|
+
}
|
|
465
|
+
if (command === 'source' && p.range != null &&
|
|
466
|
+
(!p.file || !String(p.file).trim())) {
|
|
467
|
+
return 'source range mode requires file.';
|
|
468
|
+
}
|
|
469
|
+
if (command === 'source' && p.range != null &&
|
|
470
|
+
typeof p.name === 'string' && p.name.trim()) {
|
|
471
|
+
return 'source accepts either a symbol target or a file range, not both.';
|
|
472
|
+
}
|
|
473
|
+
if (command === 'search' && p.receiver && p.type && p.type !== 'call') {
|
|
474
|
+
return 'search receiver filtering requires type=call (or omit type to select call mode).';
|
|
475
|
+
}
|
|
476
|
+
if (command === 'trace' && p.to === 'entrypoints' &&
|
|
477
|
+
(p.direction || 'callees') !== 'callers') {
|
|
478
|
+
return 'trace --to=entrypoints requires --direction=callers.';
|
|
479
|
+
}
|
|
480
|
+
if (command === 'deps' && p.cycles &&
|
|
481
|
+
(p.file || p.direction || p.depth != null || p.detailed || p.all)) {
|
|
482
|
+
return 'deps cycles mode cannot be combined with file, direction, depth, detailed, or all.';
|
|
483
|
+
}
|
|
484
|
+
if (command === 'tests' && Number(p.depth || 0) > 0 && p.callsOnly) {
|
|
485
|
+
return 'tests callsOnly applies to direct mode and cannot be combined with depth>0.';
|
|
486
|
+
}
|
|
487
|
+
if (command === 'entrypoints' && p.includeTests === true && p.excludeTests === true) {
|
|
488
|
+
return 'entrypoints includeTests and excludeTests cannot both be true.';
|
|
489
|
+
}
|
|
490
|
+
if (command === 'endpoints' && p.serverOnly && p.clientOnly) {
|
|
491
|
+
return 'endpoints serverOnly and clientOnly cannot both be true.';
|
|
492
|
+
}
|
|
493
|
+
if (command === 'plan') {
|
|
494
|
+
const operations = [p.addParam, p.removeParam, p.renameTo]
|
|
495
|
+
.filter(value => value != null && String(value).trim() !== '');
|
|
496
|
+
if (operations.length !== 1) {
|
|
497
|
+
return 'plan requires exactly one operation: addParam, removeParam, or renameTo.';
|
|
498
|
+
}
|
|
499
|
+
if (p.defaultValue != null && !p.addParam) {
|
|
500
|
+
return 'plan defaultValue is valid only with addParam.';
|
|
501
|
+
}
|
|
502
|
+
// Names that land in declarations and call sites must at least be
|
|
503
|
+
// identifier-shaped — "foo bar" or "x-y" in a rename would write
|
|
504
|
+
// syntax errors into every planned edit. Language-specific reserved
|
|
505
|
+
// words are checked after symbol resolution (plan() knows the file).
|
|
506
|
+
const IDENTIFIER_SHAPE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
507
|
+
if (p.renameTo != null && String(p.renameTo).trim() !== '' &&
|
|
508
|
+
!IDENTIFIER_SHAPE.test(String(p.renameTo).trim())) {
|
|
509
|
+
return `plan renameTo "${String(p.renameTo).trim()}" is not a valid identifier.`;
|
|
510
|
+
}
|
|
511
|
+
// addParam also accepts a typed spec (`suffix: string`, `opt?: number`)
|
|
512
|
+
// — only the NAME portion must be identifier-shaped.
|
|
513
|
+
const ADD_PARAM_SHAPE = /^[A-Za-z_$][A-Za-z0-9_$]*\??(\s*:\s*\S.*)?$/;
|
|
514
|
+
if (p.addParam != null && String(p.addParam).trim() !== '' &&
|
|
515
|
+
!ADD_PARAM_SHAPE.test(String(p.addParam).trim())) {
|
|
516
|
+
return `plan addParam "${String(p.addParam).trim()}" is not a valid parameter name (use \`name\` or \`name: type\`).`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
311
522
|
// ============================================================================
|
|
312
523
|
// COMMAND HANDLERS
|
|
313
524
|
// ============================================================================
|
|
314
525
|
|
|
315
526
|
const HANDLERS = {
|
|
316
527
|
|
|
528
|
+
// ── Public v5 compositions ─────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
show: (index, p) => {
|
|
531
|
+
const err = requireName(p.name);
|
|
532
|
+
if (err) return { ok: false, error: err };
|
|
533
|
+
const originalTarget = p.name;
|
|
534
|
+
const query = { ...p };
|
|
535
|
+
applyClassMethodSyntax(query);
|
|
536
|
+
const fileErr = checkFilePatternMatch(index, query.file);
|
|
537
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
538
|
+
const classErr = validateClassName(index, query.name, query.className);
|
|
539
|
+
if (classErr) return { ok: false, error: classErr };
|
|
540
|
+
const pinErr = checkDefinitionPin(index, query);
|
|
541
|
+
if (pinErr) return { ok: false, error: pinErr };
|
|
542
|
+
const resolved = index.resolveSymbol(query.name, {
|
|
543
|
+
file: query.file,
|
|
544
|
+
className: query.className,
|
|
545
|
+
line: query.line,
|
|
546
|
+
});
|
|
547
|
+
if (!resolved.def) return { ok: false, error: symbolNotFound(index, query.name) };
|
|
548
|
+
// A composed answer must use one exact definition in every section.
|
|
549
|
+
// Let the child handler provide the normal not-found/filter error, but
|
|
550
|
+
// once resolution succeeds pin every composition to the selected file
|
|
551
|
+
// and line. Otherwise brief/context and source can apply different
|
|
552
|
+
// legitimate ranking policies and contradict each other.
|
|
553
|
+
const targetParams = resolved.def ? {
|
|
554
|
+
...query,
|
|
555
|
+
name: resolved.def.name,
|
|
556
|
+
file: resolved.def.relativePath,
|
|
557
|
+
line: resolved.def.nameLine || resolved.def.startLine,
|
|
558
|
+
...(resolved.def.className && { className: resolved.def.className }),
|
|
559
|
+
} : query;
|
|
560
|
+
const parsed = parseSections(
|
|
561
|
+
p.sections,
|
|
562
|
+
['summary', 'callers', 'callees'],
|
|
563
|
+
SHOW_SECTIONS,
|
|
564
|
+
'show',
|
|
565
|
+
);
|
|
566
|
+
if (parsed.error) return { ok: false, error: parsed.error };
|
|
567
|
+
if (p.withTypes && !parsed.sections.includes('types')) parsed.sections.push('types');
|
|
568
|
+
|
|
569
|
+
const selected = new Set(parsed.sections);
|
|
570
|
+
const result = { target: originalTarget, sections: parsed.sections };
|
|
571
|
+
const notes = (resolved.warnings || []).map(warning => warning.message);
|
|
572
|
+
if (resolved.warnings?.length) result.warnings = resolved.warnings;
|
|
573
|
+
const run = (section, handler, params) => {
|
|
574
|
+
const response = HANDLERS[handler](index, { ...params });
|
|
575
|
+
if (!response.ok) {
|
|
576
|
+
if (!result.unavailableSections) result.unavailableSections = [];
|
|
577
|
+
result.unavailableSections.push({ section, reason: response.error });
|
|
578
|
+
notes.push(`Section "${section}" unavailable: ${response.error}`);
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
if (response.note) notes.push(response.note);
|
|
582
|
+
return response;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if (selected.has('summary')) {
|
|
586
|
+
const response = run('summary', 'brief', targetParams);
|
|
587
|
+
if (response) result.summary = response.result;
|
|
588
|
+
}
|
|
589
|
+
if (selected.has('callers') || selected.has('callees')) {
|
|
590
|
+
const response = run('relationships', 'context', targetParams);
|
|
591
|
+
if (response) {
|
|
592
|
+
// Apply the projection in the engine result so text and JSON
|
|
593
|
+
// expose the same sections. ACCOUNT/CONTRACT metadata stays
|
|
594
|
+
// attached even when one relationship direction is omitted.
|
|
595
|
+
result.context = { ...response.result };
|
|
596
|
+
if (!selected.has('callers')) {
|
|
597
|
+
delete result.context.callers;
|
|
598
|
+
delete result.context.unverifiedCallers;
|
|
599
|
+
}
|
|
600
|
+
if (!selected.has('callees')) {
|
|
601
|
+
delete result.context.callees;
|
|
602
|
+
delete result.context.unverifiedCallees;
|
|
603
|
+
}
|
|
604
|
+
result.context.omittedSections = [
|
|
605
|
+
...(!selected.has('callers') ? ['callers'] : []),
|
|
606
|
+
...(!selected.has('callees') ? ['callees'] : []),
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (selected.has('source')) {
|
|
611
|
+
const response = run('source', 'source', targetParams);
|
|
612
|
+
if (response) result.source = response.result;
|
|
613
|
+
}
|
|
614
|
+
if (selected.has('dependencies')) {
|
|
615
|
+
const response = run('dependencies', 'smart', targetParams);
|
|
616
|
+
if (response) result.dependencies = response.result;
|
|
617
|
+
}
|
|
618
|
+
if (selected.has('tests')) {
|
|
619
|
+
const response = run('tests', 'tests', { ...targetParams, depth: 0 });
|
|
620
|
+
if (response) result.tests = response.result;
|
|
621
|
+
}
|
|
622
|
+
if (selected.has('types')) {
|
|
623
|
+
const response = run('types', 'about', {
|
|
624
|
+
...targetParams, withTypes: true, compact: true,
|
|
625
|
+
});
|
|
626
|
+
if (response) {
|
|
627
|
+
result.types = {
|
|
628
|
+
types: response.result.types || [],
|
|
629
|
+
otherDefinitions: response.result.otherDefinitions || [],
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (selected.has('example')) {
|
|
634
|
+
const response = run('example', 'example', targetParams);
|
|
635
|
+
if (response) result.example = response.result;
|
|
636
|
+
}
|
|
637
|
+
if (selected.has('related')) {
|
|
638
|
+
const response = run('related', 'related', targetParams);
|
|
639
|
+
if (response) result.related = response.result;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const note = combineNotes(notes);
|
|
643
|
+
return note ? { ok: true, result, note } : { ok: true, result };
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
source: (index, p) => {
|
|
647
|
+
if (p.range != null) {
|
|
648
|
+
const response = HANDLERS.lines(index, { file: p.file, range: p.range });
|
|
649
|
+
if (response.ok) addMode(response.result, 'lines');
|
|
650
|
+
return response;
|
|
651
|
+
}
|
|
652
|
+
const err = requireName(p.name);
|
|
653
|
+
if (err) return { ok: false, error: 'Symbol name or file range is required.' };
|
|
654
|
+
const query = { ...p };
|
|
655
|
+
applyClassMethodSyntax(query);
|
|
656
|
+
const fileErr = checkFilePatternMatch(index, query.file);
|
|
657
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
658
|
+
const classErr = validateClassName(index, query.name, query.className);
|
|
659
|
+
if (classErr) return { ok: false, error: classErr };
|
|
660
|
+
const pinErr = checkDefinitionPin(index, query);
|
|
661
|
+
if (pinErr) return { ok: false, error: pinErr };
|
|
662
|
+
|
|
663
|
+
// Use the same ranker as `show`; once selected, pin extraction to the
|
|
664
|
+
// exact definition so a second handler cannot choose differently.
|
|
665
|
+
const resolved = index.resolveSymbol(query.name, {
|
|
666
|
+
file: query.file,
|
|
667
|
+
className: query.className,
|
|
668
|
+
line: query.line,
|
|
669
|
+
});
|
|
670
|
+
if (!resolved.def) return { ok: false, error: symbolNotFound(index, query.name) };
|
|
671
|
+
|
|
672
|
+
const isClass = CLASS_KIND_TYPES.includes(resolved.def.type);
|
|
673
|
+
const targetParams = query.all ? query : {
|
|
674
|
+
...query,
|
|
675
|
+
name: resolved.def.name,
|
|
676
|
+
file: resolved.def.relativePath,
|
|
677
|
+
line: resolved.def.nameLine || resolved.def.startLine,
|
|
678
|
+
...(resolved.def.className && { className: resolved.def.className }),
|
|
679
|
+
};
|
|
680
|
+
const response = HANDLERS[isClass ? 'class' : 'fn'](index, targetParams);
|
|
681
|
+
if (!response.ok) return response;
|
|
682
|
+
addMode(response.result, isClass ? 'class' : 'function');
|
|
683
|
+
const notes = [
|
|
684
|
+
...(resolved.warnings || []).map(warning => warning.message),
|
|
685
|
+
response.note,
|
|
686
|
+
].filter(Boolean);
|
|
687
|
+
return notes.length > 0
|
|
688
|
+
? { ...response, note: combineNotes(notes) }
|
|
689
|
+
: response;
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
deps: (index, p) => {
|
|
693
|
+
if (p.cycles) {
|
|
694
|
+
const response = HANDLERS.circularDeps(index, { file: p.file, exclude: p.exclude });
|
|
695
|
+
if (response.ok) addMode(response.result, 'cycles');
|
|
696
|
+
return response;
|
|
697
|
+
}
|
|
698
|
+
const err = requireFile(p.file);
|
|
699
|
+
if (err) return { ok: false, error: err };
|
|
700
|
+
const direction = p.direction || 'both';
|
|
701
|
+
if (!['imports', 'importers', 'both'].includes(direction)) {
|
|
702
|
+
return { ok: false, error: 'deps direction must be imports, importers, or both.' };
|
|
703
|
+
}
|
|
704
|
+
const graphResponse = HANDLERS.graph(index, {
|
|
705
|
+
file: p.file,
|
|
706
|
+
direction,
|
|
707
|
+
depth: p.depth,
|
|
708
|
+
all: p.all,
|
|
709
|
+
});
|
|
710
|
+
if (!graphResponse.ok) return graphResponse;
|
|
711
|
+
const result = {
|
|
712
|
+
file: p.file,
|
|
713
|
+
direction,
|
|
714
|
+
graph: graphResponse.result,
|
|
715
|
+
};
|
|
716
|
+
if (p.detailed && (direction === 'imports' || direction === 'both')) {
|
|
717
|
+
const response = HANDLERS.imports(index, { file: p.file });
|
|
718
|
+
if (!response.ok) return response;
|
|
719
|
+
result.imports = response.result;
|
|
720
|
+
}
|
|
721
|
+
if (p.detailed && (direction === 'importers' || direction === 'both')) {
|
|
722
|
+
const response = HANDLERS.exporters(index, { file: p.file });
|
|
723
|
+
if (!response.ok) return response;
|
|
724
|
+
result.importers = response.result;
|
|
725
|
+
}
|
|
726
|
+
addMode(result, 'graph');
|
|
727
|
+
return { ok: true, result };
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
repo: (index, p) => {
|
|
731
|
+
const fileErr = checkFilePatternMatch(index, p.file);
|
|
732
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
733
|
+
const inErr = validateInFilter(index, p.in);
|
|
734
|
+
if (inErr) return { ok: false, error: inErr };
|
|
735
|
+
const defaults = p.deep ? ['summary', 'health'] : ['summary'];
|
|
736
|
+
const parsed = parseSections(p.sections, defaults, REPO_SECTIONS, 'repo');
|
|
737
|
+
if (parsed.error) return { ok: false, error: parsed.error };
|
|
738
|
+
const selected = new Set(parsed.sections);
|
|
739
|
+
const result = { sections: parsed.sections };
|
|
740
|
+
const notes = [];
|
|
741
|
+
const collect = (key, handler, params) => {
|
|
742
|
+
const response = HANDLERS[handler](index, { ...params });
|
|
743
|
+
if (!response.ok) return response;
|
|
744
|
+
result[key] = response.result;
|
|
745
|
+
if (response.note) notes.push(response.note);
|
|
746
|
+
return null;
|
|
747
|
+
};
|
|
748
|
+
let failure;
|
|
749
|
+
if (selected.has('summary')) failure = collect('summary', 'orient', p);
|
|
750
|
+
if (!failure && selected.has('files')) failure = collect('files', 'toc', {
|
|
751
|
+
...p,
|
|
752
|
+
// repo's public limit caps the file result set. Direct toc's
|
|
753
|
+
// legacy symbol-list cap is intentionally not composed here.
|
|
754
|
+
top: p.top || p.limit,
|
|
755
|
+
limit: undefined,
|
|
756
|
+
});
|
|
757
|
+
if (!failure && selected.has('stats')) failure = collect('stats', 'stats', p);
|
|
758
|
+
if (!failure && selected.has('health')) failure = collect('health', 'doctor', p);
|
|
759
|
+
if (failure) return failure;
|
|
760
|
+
const note = combineNotes(notes);
|
|
761
|
+
return note ? { ok: true, result, note } : { ok: true, result };
|
|
762
|
+
},
|
|
763
|
+
|
|
317
764
|
// ── Understanding Code ──────────────────────────────────────────────
|
|
318
765
|
|
|
319
766
|
about: (index, p) => {
|
|
@@ -344,7 +791,7 @@ const HANDLERS = {
|
|
|
344
791
|
return { ok: false, error: `Symbol "${p.name}" not found in ${filterDesc}. Found ${allDefs.length} definition(s) elsewhere:\n${locations}${more}\nUse file= with a path fragment from the list above to disambiguate.` };
|
|
345
792
|
}
|
|
346
793
|
}
|
|
347
|
-
return { ok: false, error:
|
|
794
|
+
return { ok: false, error: symbolNotFound(index, p.name) };
|
|
348
795
|
}
|
|
349
796
|
const tNote = truncationNote(index);
|
|
350
797
|
return { ok: true, result, showConfidence: !!p.showConfidence, ...(tNote && { note: tNote }) };
|
|
@@ -362,15 +809,24 @@ const HANDLERS = {
|
|
|
362
809
|
if (pinErr) return { ok: false, error: pinErr };
|
|
363
810
|
const result = index.context(p.name, {
|
|
364
811
|
...buildCallerOptions(p),
|
|
812
|
+
maxCallers: num(p.top, undefined),
|
|
813
|
+
maxCallees: num(p.top, undefined),
|
|
365
814
|
unreachableOnly: !!p.unreachableOnly,
|
|
366
815
|
all: !!p.all,
|
|
367
816
|
});
|
|
368
|
-
if (!result) return { ok: false, error:
|
|
817
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name) };
|
|
369
818
|
const tNote = truncationNote(index);
|
|
370
819
|
return { ok: true, result, showConfidence: !!p.showConfidence, ...(tNote && { note: tNote }) };
|
|
371
820
|
},
|
|
372
821
|
|
|
373
822
|
impact: (index, p) => {
|
|
823
|
+
// Public v5 overload: no symbol means Git-diff impact. This replaces
|
|
824
|
+
// the separate diff-impact command without changing the symbol path.
|
|
825
|
+
if (!p.name || (typeof p.name === 'string' && !p.name.trim())) {
|
|
826
|
+
const response = HANDLERS.diffImpact(index, { ...p });
|
|
827
|
+
if (response.ok) addMode(response.result, 'diff');
|
|
828
|
+
return response;
|
|
829
|
+
}
|
|
374
830
|
const err = requireName(p.name);
|
|
375
831
|
if (err) return { ok: false, error: err };
|
|
376
832
|
applyClassMethodSyntax(p);
|
|
@@ -383,8 +839,9 @@ const HANDLERS = {
|
|
|
383
839
|
const result = index.impact(p.name, {
|
|
384
840
|
file: p.file,
|
|
385
841
|
className: p.className,
|
|
842
|
+
line: p.line,
|
|
386
843
|
exclude: toExcludeArray(p.exclude),
|
|
387
|
-
top: num(p.top, undefined),
|
|
844
|
+
top: num(p.limit, undefined) || num(p.top, undefined),
|
|
388
845
|
unreachableOnly: !!p.unreachableOnly,
|
|
389
846
|
// BUG-H3: pass through user-supplied flags. impact defaults to including
|
|
390
847
|
// method calls because "what breaks if I change this" should include
|
|
@@ -393,8 +850,9 @@ const HANDLERS = {
|
|
|
393
850
|
...(p.includeMethods !== undefined && { includeMethods: p.includeMethods }),
|
|
394
851
|
...(p.includeUncertain !== undefined && { includeUncertain: p.includeUncertain }),
|
|
395
852
|
});
|
|
396
|
-
if (!result) return { ok: false, error:
|
|
853
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
397
854
|
const tNote = truncationNote(index);
|
|
855
|
+
addMode(result, 'symbol');
|
|
398
856
|
return { ok: true, result, ...(tNote && { note: tNote }) };
|
|
399
857
|
},
|
|
400
858
|
|
|
@@ -412,10 +870,10 @@ const HANDLERS = {
|
|
|
412
870
|
const result = index.blast(p.name, {
|
|
413
871
|
...buildCallerOptions(p),
|
|
414
872
|
depth: depthVal ?? 3,
|
|
415
|
-
all: p.all
|
|
873
|
+
all: !!p.all,
|
|
416
874
|
expandUnverified: !!p.expandUnverified,
|
|
417
875
|
});
|
|
418
|
-
if (!result) return { ok: false, error:
|
|
876
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
419
877
|
const note = treeNote(result);
|
|
420
878
|
const tNote = truncationNote(index);
|
|
421
879
|
const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
|
|
@@ -436,10 +894,10 @@ const HANDLERS = {
|
|
|
436
894
|
const result = index.reverseTrace(p.name, {
|
|
437
895
|
...buildCallerOptions(p),
|
|
438
896
|
depth: depthVal ?? 5,
|
|
439
|
-
all: p.all
|
|
897
|
+
all: !!p.all,
|
|
440
898
|
expandUnverified: !!p.expandUnverified,
|
|
441
899
|
});
|
|
442
|
-
if (!result) return { ok: false, error:
|
|
900
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
443
901
|
const note = treeNote(result);
|
|
444
902
|
const tNote = truncationNote(index);
|
|
445
903
|
const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
|
|
@@ -460,12 +918,29 @@ const HANDLERS = {
|
|
|
460
918
|
...buildCallerOptions(p),
|
|
461
919
|
withTypes: p.withTypes || false,
|
|
462
920
|
});
|
|
463
|
-
if (!result) return { ok: false, error:
|
|
921
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
464
922
|
const tNote = truncationNote(index);
|
|
465
923
|
return { ok: true, result, ...(tNote && { note: tNote }) };
|
|
466
924
|
},
|
|
467
925
|
|
|
468
926
|
trace: (index, p) => {
|
|
927
|
+
const direction = p.direction || 'callees';
|
|
928
|
+
if (!['callees', 'callers'].includes(direction)) {
|
|
929
|
+
return { ok: false, error: 'trace direction must be callees or callers.' };
|
|
930
|
+
}
|
|
931
|
+
if (p.to && p.to !== 'entrypoints') {
|
|
932
|
+
return { ok: false, error: 'trace --to currently accepts only entrypoints.' };
|
|
933
|
+
}
|
|
934
|
+
if (direction === 'callers') {
|
|
935
|
+
const response = p.to === 'entrypoints'
|
|
936
|
+
? HANDLERS.reverseTrace(index, { ...p, direction: undefined, to: undefined })
|
|
937
|
+
: HANDLERS.blast(index, { ...p, direction: undefined, to: undefined });
|
|
938
|
+
if (response.ok) addMode(response.result, p.to === 'entrypoints' ? 'entrypoints' : 'callers');
|
|
939
|
+
return response;
|
|
940
|
+
}
|
|
941
|
+
if (p.to) {
|
|
942
|
+
return { ok: false, error: 'trace --to=entrypoints requires --direction=callers.' };
|
|
943
|
+
}
|
|
469
944
|
const err = requireName(p.name);
|
|
470
945
|
if (err) return { ok: false, error: err };
|
|
471
946
|
applyClassMethodSyntax(p);
|
|
@@ -479,13 +954,14 @@ const HANDLERS = {
|
|
|
479
954
|
const result = index.trace(p.name, {
|
|
480
955
|
...buildCallerOptions(p),
|
|
481
956
|
depth: depthVal ?? 3,
|
|
482
|
-
all: p.all
|
|
957
|
+
all: !!p.all,
|
|
483
958
|
expandUnverified: !!p.expandUnverified,
|
|
484
959
|
});
|
|
485
|
-
if (!result) return { ok: false, error:
|
|
960
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
486
961
|
const note = treeNote(result);
|
|
487
962
|
const tNote = truncationNote(index);
|
|
488
963
|
const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
|
|
964
|
+
addMode(result, 'callees');
|
|
489
965
|
return { ok: true, result, ...(combined && { note: combined }) };
|
|
490
966
|
},
|
|
491
967
|
|
|
@@ -537,7 +1013,7 @@ const HANDLERS = {
|
|
|
537
1013
|
top: num(p.top, undefined),
|
|
538
1014
|
all: p.all,
|
|
539
1015
|
});
|
|
540
|
-
if (!result) return { ok: false, error:
|
|
1016
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
541
1017
|
const parts = [];
|
|
542
1018
|
if (result.similarNamesTotal > result.similarNames.length)
|
|
543
1019
|
parts.push(`similar names: showing ${result.similarNames.length} of ${result.similarNamesTotal}`);
|
|
@@ -565,7 +1041,7 @@ const HANDLERS = {
|
|
|
565
1041
|
// Thread the line pin (fix #249: a `lib.js:5:run` handle silently
|
|
566
1042
|
// resolved the OTHER same-name def — the pin's whole point).
|
|
567
1043
|
const result = brief(index, p.name, { file: p.file, className: p.className, line: p.line, git: !!p.git });
|
|
568
|
-
if (!result) return { ok: false, error:
|
|
1044
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name) };
|
|
569
1045
|
return { ok: true, result };
|
|
570
1046
|
},
|
|
571
1047
|
|
|
@@ -579,7 +1055,12 @@ const HANDLERS = {
|
|
|
579
1055
|
top = Math.min(n, 100);
|
|
580
1056
|
}
|
|
581
1057
|
const { orient } = require('./reporting');
|
|
582
|
-
const result = orient(index, {
|
|
1058
|
+
const result = orient(index, {
|
|
1059
|
+
top,
|
|
1060
|
+
file: p.file,
|
|
1061
|
+
in: p.in,
|
|
1062
|
+
exclude: toExcludeArray(p.exclude),
|
|
1063
|
+
});
|
|
583
1064
|
return { ok: true, result };
|
|
584
1065
|
},
|
|
585
1066
|
|
|
@@ -588,12 +1069,20 @@ const HANDLERS = {
|
|
|
588
1069
|
const result = doctor(index, {
|
|
589
1070
|
in: p.in,
|
|
590
1071
|
file: p.file,
|
|
1072
|
+
exclude: toExcludeArray(p.exclude),
|
|
591
1073
|
deep: !!p.deep,
|
|
592
1074
|
});
|
|
593
1075
|
return { ok: true, result };
|
|
594
1076
|
},
|
|
595
1077
|
|
|
596
1078
|
check: (index, p) => {
|
|
1079
|
+
// Public v5 overload: a symbol target performs the former verify
|
|
1080
|
+
// operation; a target-less invocation remains the diff/precommit check.
|
|
1081
|
+
if (p.name && String(p.name).trim()) {
|
|
1082
|
+
const response = HANDLERS.verify(index, { ...p });
|
|
1083
|
+
if (response.ok) addMode(response.result, 'symbol');
|
|
1084
|
+
return response;
|
|
1085
|
+
}
|
|
597
1086
|
const { check } = require('./check');
|
|
598
1087
|
try {
|
|
599
1088
|
const result = check(index, {
|
|
@@ -602,6 +1091,7 @@ const HANDLERS = {
|
|
|
602
1091
|
file: p.file,
|
|
603
1092
|
limit: num(p.limit, undefined),
|
|
604
1093
|
});
|
|
1094
|
+
addMode(result, 'diff');
|
|
605
1095
|
return { ok: true, result };
|
|
606
1096
|
} catch (e) {
|
|
607
1097
|
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
@@ -611,6 +1101,35 @@ const HANDLERS = {
|
|
|
611
1101
|
// ── Finding Code ────────────────────────────────────────────────────
|
|
612
1102
|
|
|
613
1103
|
find: (index, p) => {
|
|
1104
|
+
const inErr = validateInFilter(index, p.in);
|
|
1105
|
+
if (inErr) return { ok: false, error: inErr };
|
|
1106
|
+
const validFindTypes = new Set([
|
|
1107
|
+
'type', 'function', 'class', 'variable', 'state', 'constant',
|
|
1108
|
+
'field', 'macro', 'method', 'constructor', 'interface', 'enum',
|
|
1109
|
+
'struct', 'trait', 'record', 'namespace',
|
|
1110
|
+
]);
|
|
1111
|
+
if (p.type && !validFindTypes.has(p.type)) {
|
|
1112
|
+
return { ok: false, error: `Invalid find type "${p.type}". Valid types: ${[...validFindTypes].join(', ')}.` };
|
|
1113
|
+
}
|
|
1114
|
+
if (p.type === 'type') {
|
|
1115
|
+
const response = HANDLERS.typedef(index, { ...p });
|
|
1116
|
+
if (response.ok) {
|
|
1117
|
+
if (!p.withSource && Array.isArray(response.result)) {
|
|
1118
|
+
response.result = response.result.map(({ code, ...item }) => item);
|
|
1119
|
+
}
|
|
1120
|
+
const limit = num(p.limit, undefined);
|
|
1121
|
+
if (limit && limit > 0 && Array.isArray(response.result)) {
|
|
1122
|
+
const { items, total, limited } = applyLimit(response.result, limit);
|
|
1123
|
+
response.result = items;
|
|
1124
|
+
if (limited) {
|
|
1125
|
+
const note = limitNote(limit, total);
|
|
1126
|
+
response.note = response.note ? `${response.note}\n${note}` : note;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
addMode(response.result, 'type');
|
|
1130
|
+
}
|
|
1131
|
+
return response;
|
|
1132
|
+
}
|
|
614
1133
|
const err = requireName(p.name);
|
|
615
1134
|
if (err) return { ok: false, error: err };
|
|
616
1135
|
applyClassMethodSyntax(p);
|
|
@@ -621,12 +1140,15 @@ const HANDLERS = {
|
|
|
621
1140
|
const classErr = validateClassName(index, p.name, p.className);
|
|
622
1141
|
if (classErr) return { ok: false, error: classErr };
|
|
623
1142
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (includeTests === undefined && p.name && /^test[_*?A-Z]/i.test(p.name)) {
|
|
628
|
-
includeTests = true;
|
|
1143
|
+
if (p.line) {
|
|
1144
|
+
const pinErr = checkDefinitionPin(index, p);
|
|
1145
|
+
if (pinErr) return { ok: false, error: pinErr };
|
|
629
1146
|
}
|
|
1147
|
+
// Symbol lookup is an inventory operation: a definition must not
|
|
1148
|
+
// disappear merely because it lives in a test file. Call-bearing
|
|
1149
|
+
// analysis commands retain their conservative test defaults; find
|
|
1150
|
+
// includes every indexed definition unless explicitly asked not to.
|
|
1151
|
+
const includeTests = p.includeTests !== false;
|
|
630
1152
|
const exclude = applyTestExclusions(p.exclude, includeTests);
|
|
631
1153
|
let result = index.find(p.name, {
|
|
632
1154
|
file: p.file,
|
|
@@ -635,11 +1157,49 @@ const HANDLERS = {
|
|
|
635
1157
|
exclude,
|
|
636
1158
|
in: p.in,
|
|
637
1159
|
});
|
|
1160
|
+
const line = Number(p.line);
|
|
1161
|
+
if (p.line && Number.isFinite(line)) {
|
|
1162
|
+
result = result.filter(item =>
|
|
1163
|
+
item.startLine === line || item.nameLine === line);
|
|
1164
|
+
}
|
|
1165
|
+
if (p.type) {
|
|
1166
|
+
const kindGroups = {
|
|
1167
|
+
function: new Set(['function', 'method', 'constructor', 'get', 'set']),
|
|
1168
|
+
class: new Set(CLASS_KIND_TYPES),
|
|
1169
|
+
variable: new Set(['state', 'constant', 'field']),
|
|
1170
|
+
};
|
|
1171
|
+
const kinds = kindGroups[p.type] || new Set([p.type]);
|
|
1172
|
+
result = result.filter(item => kinds.has(item.type));
|
|
1173
|
+
}
|
|
1174
|
+
if (p.withSource) {
|
|
1175
|
+
result = result.map(item => ({ ...item, code: readAndExtract(item) }));
|
|
1176
|
+
}
|
|
1177
|
+
const fullFindCount = result.length;
|
|
1178
|
+
const nameWideDefinitionCounts = Object.fromEntries(
|
|
1179
|
+
[...new Set(result.map(item => item.name))].map(name => [
|
|
1180
|
+
name,
|
|
1181
|
+
(index.symbols.get(name) || []).length,
|
|
1182
|
+
]),
|
|
1183
|
+
);
|
|
638
1184
|
// Warn if exact mode silently disables glob expansion
|
|
639
1185
|
const notes = [];
|
|
640
1186
|
if (p.exact && p.name && (p.name.includes('*') || p.name.includes('?'))) {
|
|
641
1187
|
notes.push(`Note: exact=true treats "${p.name}" as a literal name (glob expansion disabled).`);
|
|
642
1188
|
}
|
|
1189
|
+
let unsupportedMatches = null;
|
|
1190
|
+
if (fullFindCount === 0 && index.unsupportedFiles?.length > 0) {
|
|
1191
|
+
unsupportedMatches = require('./account').scanUnsupportedFiles(index, p.name);
|
|
1192
|
+
const languages = Object.keys(unsupportedMatches.languages).join(', ');
|
|
1193
|
+
if (unsupportedMatches.lines > 0) {
|
|
1194
|
+
notes.push(`${unsupportedMatches.lines} literal match(es) exist in ` +
|
|
1195
|
+
`${unsupportedMatches.fileCount} unsupported-language file(s)` +
|
|
1196
|
+
`${languages ? ` (${languages})` : ''}; this is not a semantic zero. ` +
|
|
1197
|
+
'Use grep/ripgrep plus a language-native analyzer.');
|
|
1198
|
+
} else {
|
|
1199
|
+
notes.push(`UCN skipped ${index.unsupportedFiles.length} unsupported source ` +
|
|
1200
|
+
'file(s), so this is not a repository-wide semantic zero; verify with grep/ripgrep.');
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
643
1203
|
// Apply limit
|
|
644
1204
|
const limit = num(p.limit, undefined);
|
|
645
1205
|
if (limit && limit > 0) {
|
|
@@ -647,6 +1207,20 @@ const HANDLERS = {
|
|
|
647
1207
|
if (limited) notes.push(limitNote(limit, total));
|
|
648
1208
|
result = items;
|
|
649
1209
|
}
|
|
1210
|
+
Object.defineProperty(result, 'findInfo', {
|
|
1211
|
+
value: {
|
|
1212
|
+
total: fullFindCount,
|
|
1213
|
+
shown: result.length,
|
|
1214
|
+
nameWideDefinitionCounts,
|
|
1215
|
+
},
|
|
1216
|
+
enumerable: false, writable: true, configurable: true,
|
|
1217
|
+
});
|
|
1218
|
+
if (unsupportedMatches) {
|
|
1219
|
+
Object.defineProperty(result, 'unsupportedMatches', {
|
|
1220
|
+
value: unsupportedMatches,
|
|
1221
|
+
enumerable: false, writable: true, configurable: true,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
650
1224
|
const tNote = truncationNote(index);
|
|
651
1225
|
if (tNote) notes.push(tNote);
|
|
652
1226
|
return { ok: true, result, note: notes.length ? notes.join('\n') : undefined };
|
|
@@ -656,6 +1230,8 @@ const HANDLERS = {
|
|
|
656
1230
|
const err = requireName(p.name);
|
|
657
1231
|
if (err) return { ok: false, error: err };
|
|
658
1232
|
applyClassMethodSyntax(p);
|
|
1233
|
+
const inErr = validateInFilter(index, p.in);
|
|
1234
|
+
if (inErr) return { ok: false, error: inErr };
|
|
659
1235
|
const exclude = applyTestExclusions(p.exclude, p.includeTests);
|
|
660
1236
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
661
1237
|
if (fileErr) return { ok: false, error: fileErr };
|
|
@@ -675,7 +1251,11 @@ const HANDLERS = {
|
|
|
675
1251
|
codeOnly: p.codeOnly || false,
|
|
676
1252
|
context: num(p.context, 0),
|
|
677
1253
|
className: p.className,
|
|
678
|
-
|
|
1254
|
+
// A stable handle pins which definition is meant; it must not
|
|
1255
|
+
// silently turn into a same-file-only usage scan. An explicit
|
|
1256
|
+
// --file remains a deliberate scan scope.
|
|
1257
|
+
file: p._fileFromHandle ? undefined : p.file,
|
|
1258
|
+
definitionFile: p._fileFromHandle ? p.file : undefined,
|
|
679
1259
|
exclude: userExclude,
|
|
680
1260
|
in: p.in,
|
|
681
1261
|
});
|
|
@@ -705,11 +1285,44 @@ const HANDLERS = {
|
|
|
705
1285
|
// record that isn't a call or import is a reference —
|
|
706
1286
|
// same-name definer sites (usageType 'definition' with
|
|
707
1287
|
// isDefinition false) used to render in NO band.
|
|
708
|
-
references: result.filter(u => !u.isDefinition &&
|
|
1288
|
+
references: result.filter(u => !u.isDefinition &&
|
|
1289
|
+
!['call', 'import', 'text', 'definition'].includes(u.usageType)).length,
|
|
1290
|
+
otherDefinitions: result.filter(u => !u.isDefinition &&
|
|
1291
|
+
u.usageType === 'definition').length,
|
|
1292
|
+
text: result.filter(u => u.usageType === 'text').length,
|
|
709
1293
|
},
|
|
710
1294
|
enumerable: false, writable: true, configurable: true,
|
|
711
1295
|
});
|
|
712
1296
|
}
|
|
1297
|
+
// Escape-hatch integrity: usages is the raw name-match view, so it
|
|
1298
|
+
// must not silently omit matches in files UCN cannot parse (.rb in a
|
|
1299
|
+
// mixed repo). Counts ride a note + non-enumerable field; the
|
|
1300
|
+
// account-bearing commands list the actual sites.
|
|
1301
|
+
const accountTools = require('./account');
|
|
1302
|
+
const unsupportedMatches = accountTools.scanUnsupportedFiles(index, p.name);
|
|
1303
|
+
if (unsupportedMatches.lines > 0) {
|
|
1304
|
+
const langs = Object.keys(unsupportedMatches.languages).join(', ');
|
|
1305
|
+
notes.push(`${unsupportedMatches.lines} line(s) in ${unsupportedMatches.fileCount} ` +
|
|
1306
|
+
`unsupported-language file(s)${langs ? ` (${langs})` : ''} also match — ` +
|
|
1307
|
+
'NOT analyzed by UCN; verify with grep/ripgrep.');
|
|
1308
|
+
Object.defineProperty(limited, 'unsupportedMatches', {
|
|
1309
|
+
value: unsupportedMatches,
|
|
1310
|
+
enumerable: false, writable: true, configurable: true,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
const failed = accountTools.scanFailedFiles(index, p.name);
|
|
1314
|
+
if (failed.unparsed.lines > 0) {
|
|
1315
|
+
notes.push(`${failed.unparsed.lines} matching line(s) in ${failed.unparsed.fileCount} unparsed file(s) were not classified: ${failed.unparsed.files.join(', ')}.`);
|
|
1316
|
+
}
|
|
1317
|
+
if (failed.unreadableFiles.length > 0) {
|
|
1318
|
+
notes.push(`${failed.unreadableFiles.length} unreadable file(s) could not be searched: ${failed.unreadableFiles.join(', ')}.`);
|
|
1319
|
+
}
|
|
1320
|
+
if (failed.unparsed.lines > 0 || failed.unreadableFiles.length > 0) {
|
|
1321
|
+
Object.defineProperty(limited, 'analysisGaps', {
|
|
1322
|
+
value: failed,
|
|
1323
|
+
enumerable: false, writable: true, configurable: true,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
713
1326
|
return { ok: true, result: limited, note: notes.length ? notes.join(' ') : undefined };
|
|
714
1327
|
},
|
|
715
1328
|
|
|
@@ -749,8 +1362,6 @@ const HANDLERS = {
|
|
|
749
1362
|
if (remaining <= 0) {
|
|
750
1363
|
if (syms.functions) syms.functions = [];
|
|
751
1364
|
if (syms.classes) syms.classes = [];
|
|
752
|
-
f.functions = 0;
|
|
753
|
-
f.classes = 0;
|
|
754
1365
|
continue;
|
|
755
1366
|
}
|
|
756
1367
|
const fns = syms.functions?.length || 0;
|
|
@@ -761,15 +1372,12 @@ const HANDLERS = {
|
|
|
761
1372
|
if (syms.functions && remaining > 0) {
|
|
762
1373
|
syms.functions = syms.functions.slice(0, remaining);
|
|
763
1374
|
remaining -= syms.functions.length;
|
|
764
|
-
f.functions = syms.functions.length;
|
|
765
1375
|
}
|
|
766
1376
|
if (syms.classes && remaining > 0) {
|
|
767
1377
|
syms.classes = syms.classes.slice(0, remaining);
|
|
768
1378
|
remaining -= syms.classes.length;
|
|
769
|
-
f.classes = syms.classes.length;
|
|
770
1379
|
} else if (syms.classes) {
|
|
771
1380
|
syms.classes = [];
|
|
772
|
-
f.classes = 0;
|
|
773
1381
|
}
|
|
774
1382
|
}
|
|
775
1383
|
}
|
|
@@ -805,15 +1413,30 @@ const HANDLERS = {
|
|
|
805
1413
|
top: topVal || 50,
|
|
806
1414
|
});
|
|
807
1415
|
if (result.meta.error) return { ok: false, error: result.meta.error };
|
|
808
|
-
|
|
1416
|
+
const unsupported = (!p.regex && (p.term || p.name))
|
|
1417
|
+
? require('./account').scanUnsupportedFiles(index, p.term || p.name)
|
|
1418
|
+
: null;
|
|
1419
|
+
let note;
|
|
1420
|
+
if (unsupported?.lines > 0) {
|
|
1421
|
+
Object.defineProperty(result, 'unsupportedMatches', {
|
|
1422
|
+
value: unsupported,
|
|
1423
|
+
enumerable: false, writable: true, configurable: true,
|
|
1424
|
+
});
|
|
1425
|
+
note = `${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not structurally analyzed; verify with grep/ripgrep.`;
|
|
1426
|
+
}
|
|
1427
|
+
return { ok: true, result, structural: true, note };
|
|
809
1428
|
}
|
|
810
1429
|
|
|
811
1430
|
const err = requireTerm(p.term);
|
|
812
1431
|
if (err) return { ok: false, error: err };
|
|
813
1432
|
const testsExcluded = !p.includeTests;
|
|
814
1433
|
const exclude = applyTestExclusions(p.exclude, p.includeTests);
|
|
815
|
-
//
|
|
1434
|
+
// JSON is intentionally structured rather than byte-truncated, so
|
|
1435
|
+
// text search needs a row bound of its own. Keep the documented
|
|
1436
|
+
// default at 500 and retain total/truncated metadata for controlled
|
|
1437
|
+
// expansion with an explicit top/limit.
|
|
816
1438
|
const topVal = num(p.top, undefined) || num(p.limit, undefined);
|
|
1439
|
+
const effectiveLimit = topVal || 500;
|
|
817
1440
|
const result = index.search(p.term, {
|
|
818
1441
|
codeOnly: p.codeOnly || false,
|
|
819
1442
|
context: num(p.context, 0),
|
|
@@ -821,15 +1444,33 @@ const HANDLERS = {
|
|
|
821
1444
|
exclude,
|
|
822
1445
|
in: p.in,
|
|
823
1446
|
regex: p.regex,
|
|
824
|
-
top:
|
|
1447
|
+
top: effectiveLimit,
|
|
825
1448
|
file: p.file,
|
|
826
1449
|
});
|
|
827
1450
|
if (result.meta) result.meta.testsExcluded = testsExcluded;
|
|
1451
|
+
const notes = [];
|
|
1452
|
+
if (!p.regex) {
|
|
1453
|
+
const unsupported = require('./account').scanUnsupportedFiles(index, p.term);
|
|
1454
|
+
if (unsupported.lines > 0) {
|
|
1455
|
+
Object.defineProperty(result, 'unsupportedMatches', {
|
|
1456
|
+
value: unsupported,
|
|
1457
|
+
enumerable: false, writable: true, configurable: true,
|
|
1458
|
+
});
|
|
1459
|
+
notes.push(`${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not searched; verify with grep/ripgrep.`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
828
1462
|
const tNote = truncationNote(index);
|
|
829
|
-
|
|
1463
|
+
if (tNote) notes.push(tNote);
|
|
1464
|
+
return { ok: true, result, ...(notes.length && { note: notes.join('\n') }) };
|
|
830
1465
|
},
|
|
831
1466
|
|
|
832
1467
|
tests: (index, p) => {
|
|
1468
|
+
const depth = num(p.depth, 0);
|
|
1469
|
+
if (depth > 0) {
|
|
1470
|
+
const response = HANDLERS.affectedTests(index, { ...p, depth });
|
|
1471
|
+
if (response.ok) addMode(response.result, 'affected');
|
|
1472
|
+
return response;
|
|
1473
|
+
}
|
|
833
1474
|
const err = requireName(p.name);
|
|
834
1475
|
if (err) return { ok: false, error: err };
|
|
835
1476
|
// tests() accepts a FILE PATH as the target ("tests helper.go" — the
|
|
@@ -844,8 +1485,12 @@ const HANDLERS = {
|
|
|
844
1485
|
if (!testsTargetIsFile) applyClassMethodSyntax(p);
|
|
845
1486
|
if (testsTargetIsHandle && p.line) {
|
|
846
1487
|
const line = Number(p.line);
|
|
1488
|
+
const resolvedFile = p.file ? index.resolveFilePathForQuery(p.file) : null;
|
|
847
1489
|
const pinned = (index.symbols.get(p.name) || []).filter(d =>
|
|
848
|
-
d.startLine === line
|
|
1490
|
+
(d.startLine === line || d.nameLine === line) &&
|
|
1491
|
+
(!p.file || (typeof resolvedFile === 'string'
|
|
1492
|
+
? d.file === resolvedFile
|
|
1493
|
+
: d.relativePath?.includes(p.file))));
|
|
849
1494
|
if (pinned.length === 1 && !p.className && pinned[0].className) {
|
|
850
1495
|
p.className = pinned[0].className;
|
|
851
1496
|
}
|
|
@@ -863,18 +1508,59 @@ const HANDLERS = {
|
|
|
863
1508
|
const files = allDefs.map(d => d.relativePath).join(', ');
|
|
864
1509
|
return { ok: false, error: `Symbol "${p.name}" not found in files matching "${p.file}". Defined in: ${files}` };
|
|
865
1510
|
}
|
|
866
|
-
return { ok: false, error:
|
|
1511
|
+
return { ok: false, error: symbolNotFound(index, p.name) };
|
|
867
1512
|
}
|
|
868
1513
|
}
|
|
869
1514
|
const classErr = validateClassName(index, p.name, p.className);
|
|
870
1515
|
if (classErr) return { ok: false, error: classErr };
|
|
1516
|
+
let testsResolution = null;
|
|
1517
|
+
if (!testsTargetIsFile) {
|
|
1518
|
+
testsResolution = index.resolveSymbol(p.name, {
|
|
1519
|
+
file: p.file,
|
|
1520
|
+
className: p.className,
|
|
1521
|
+
line: p.line,
|
|
1522
|
+
});
|
|
1523
|
+
if (!testsResolution.def) return { ok: false, error: symbolNotFound(index, p.name) };
|
|
1524
|
+
}
|
|
871
1525
|
const result = index.tests(p.name, {
|
|
872
1526
|
callsOnly: p.callsOnly || false,
|
|
873
1527
|
className: p.className,
|
|
874
|
-
|
|
1528
|
+
// Bare ambiguous names use the same deterministic definition as
|
|
1529
|
+
// the rest of the semantic surface. The warning below discloses
|
|
1530
|
+
// that choice and gives the caller a stable file= escape hatch.
|
|
1531
|
+
file: p.file || testsResolution?.def?.relativePath,
|
|
875
1532
|
exclude: toExcludeArray(p.exclude),
|
|
876
1533
|
});
|
|
877
|
-
|
|
1534
|
+
addMode(result, 'direct');
|
|
1535
|
+
if (testsResolution?.warnings?.length > 0) {
|
|
1536
|
+
Object.defineProperty(result, 'warnings', {
|
|
1537
|
+
value: testsResolution.warnings,
|
|
1538
|
+
enumerable: false, writable: true, configurable: true,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
const testNotes = [];
|
|
1542
|
+
if (result.length === 0) testNotes.push(NO_STATIC_TEST_LINK_NOTE);
|
|
1543
|
+
// Mixed-language honesty: test files in unsupported languages (RSpec
|
|
1544
|
+
// for a .rb service, etc.) are invisible to the static scan — say so
|
|
1545
|
+
// whenever the target name occurs in unsupported files at all.
|
|
1546
|
+
if (!testsTargetIsFile) {
|
|
1547
|
+
const unsupportedMatches = require('./account').scanUnsupportedFiles(index, p.name);
|
|
1548
|
+
if (unsupportedMatches.lines > 0) {
|
|
1549
|
+
const langs = Object.keys(unsupportedMatches.languages).join(', ');
|
|
1550
|
+
testNotes.push(`"${p.name}" also occurs on ${unsupportedMatches.lines} line(s) in ` +
|
|
1551
|
+
`${unsupportedMatches.fileCount} unsupported-language file(s)${langs ? ` (${langs})` : ''} — ` +
|
|
1552
|
+
'their test files are invisible to UCN; verify with grep/ripgrep.');
|
|
1553
|
+
Object.defineProperty(result, 'unsupportedMatches', {
|
|
1554
|
+
value: unsupportedMatches,
|
|
1555
|
+
enumerable: false, writable: true, configurable: true,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
ok: true,
|
|
1561
|
+
result,
|
|
1562
|
+
...(testNotes.length > 0 && { note: testNotes.join(' ') }),
|
|
1563
|
+
};
|
|
878
1564
|
},
|
|
879
1565
|
|
|
880
1566
|
affectedTests: (index, p) => {
|
|
@@ -892,10 +1578,11 @@ const HANDLERS = {
|
|
|
892
1578
|
...buildCallerOptions(p),
|
|
893
1579
|
depth: depthVal ?? 3,
|
|
894
1580
|
});
|
|
895
|
-
if (!result) return { ok: false, error:
|
|
1581
|
+
if (!result) return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
896
1582
|
const note = treeNote(result);
|
|
897
1583
|
const tNote = truncationNote(index);
|
|
898
|
-
const
|
|
1584
|
+
const selectionNote = result.testFiles.length === 0 ? NO_STATIC_TEST_LINK_NOTE : null;
|
|
1585
|
+
const combined = [note, tNote, selectionNote].filter(Boolean).join('\n') || undefined;
|
|
899
1586
|
return { ok: true, result, ...(combined && { note: combined }) };
|
|
900
1587
|
},
|
|
901
1588
|
|
|
@@ -929,6 +1616,11 @@ const HANDLERS = {
|
|
|
929
1616
|
if (result.excludedExported != null) sliced.excludedExported = result.excludedExported;
|
|
930
1617
|
if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
|
|
931
1618
|
if (result.excludedExternalContract != null) sliced.excludedExternalContract = result.excludedExternalContract;
|
|
1619
|
+
if (result.excludedRuntimeContract != null) sliced.excludedRuntimeContract = result.excludedRuntimeContract;
|
|
1620
|
+
if (result.excludedDynamicDispatch != null) sliced.excludedDynamicDispatch = result.excludedDynamicDispatch;
|
|
1621
|
+
if (result.pythonImplicitExportFiles != null) sliced.pythonImplicitExportFiles = result.pythonImplicitExportFiles;
|
|
1622
|
+
if (result.computedDispatch != null) sliced.computedDispatch = result.computedDispatch;
|
|
1623
|
+
if (result.coverage != null) sliced.coverage = result.coverage;
|
|
932
1624
|
// Truncation must be visible IN the JSON payload, not only in the
|
|
933
1625
|
// stderr note (fix #242) — the formatter reads this to emit
|
|
934
1626
|
// meta.total + truncated.
|
|
@@ -951,26 +1643,53 @@ const HANDLERS = {
|
|
|
951
1643
|
// Rust #[test], etc.) — show them by default. Previously this command
|
|
952
1644
|
// applied addTestExclusions() unconditionally, which stripped Java
|
|
953
1645
|
// *Tests.java entries while letting Rust #[test] through (asymmetric).
|
|
954
|
-
//
|
|
955
|
-
//
|
|
1646
|
+
// Tests are excluded by default, matching search/usages/deadcode —
|
|
1647
|
+
// an agent orienting via entrypoints needs the routes that describe
|
|
1648
|
+
// the project, not every fixture route under test/. --include-tests
|
|
1649
|
+
// restores the full universe; --exclude-tests stays accepted as the
|
|
1650
|
+
// explicit spelling of the default. Internal consumers (reachability
|
|
1651
|
+
// seeding, check's orphan detection) call detectEntrypoints directly
|
|
1652
|
+
// and always see the full universe.
|
|
956
1653
|
const userExclude = Array.isArray(p.exclude)
|
|
957
1654
|
? p.exclude
|
|
958
1655
|
: (p.exclude ? p.exclude.split(',').map(s => s.trim()).filter(Boolean) : []);
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1656
|
+
const exclude = p.includeTests === true
|
|
1657
|
+
? userExclude
|
|
1658
|
+
: addTestExclusions(userExclude);
|
|
1659
|
+
const fullResult = detectEntrypoints(index, {
|
|
962
1660
|
type: p.type,
|
|
963
1661
|
framework: p.framework,
|
|
964
1662
|
file: p.file,
|
|
965
|
-
exclude,
|
|
1663
|
+
exclude: userExclude,
|
|
966
1664
|
});
|
|
1665
|
+
if (fullResult && fullResult.error) {
|
|
1666
|
+
return { ok: false, error: fullResult.message || fullResult.error };
|
|
1667
|
+
}
|
|
1668
|
+
let result = p.includeTests === true
|
|
1669
|
+
? fullResult
|
|
1670
|
+
: detectEntrypoints(index, {
|
|
1671
|
+
type: p.type,
|
|
1672
|
+
framework: p.framework,
|
|
1673
|
+
file: p.file,
|
|
1674
|
+
exclude,
|
|
1675
|
+
});
|
|
967
1676
|
if (result && result.error) {
|
|
968
1677
|
return { ok: false, error: result.message || result.error };
|
|
969
1678
|
}
|
|
1679
|
+
const hiddenTests = p.includeTests === true || !Array.isArray(fullResult)
|
|
1680
|
+
? 0
|
|
1681
|
+
: Math.max(0, fullResult.length - result.length);
|
|
1682
|
+
Object.defineProperty(result, 'filterInfo', {
|
|
1683
|
+
value: { hiddenTests, testsIncluded: p.includeTests === true },
|
|
1684
|
+
enumerable: false, writable: true, configurable: true,
|
|
1685
|
+
});
|
|
970
1686
|
const limit = num(p.limit, undefined);
|
|
971
|
-
let note
|
|
1687
|
+
let note = hiddenTests > 0
|
|
1688
|
+
? `${hiddenTests} test-path entry point(s) hidden by default. Use --include-tests to include them.`
|
|
1689
|
+
: undefined;
|
|
972
1690
|
if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
|
|
973
|
-
|
|
1691
|
+
const limitMessage = limitNote(limit, result.length);
|
|
1692
|
+
note = note ? `${note}\n${limitMessage}` : limitMessage;
|
|
974
1693
|
const sliced = result.slice(0, limit);
|
|
975
1694
|
// Full-set size travels with the payload so --json can carry
|
|
976
1695
|
// meta.total + truncated (fix #247 — mirrors deadcode's #242
|
|
@@ -979,6 +1698,10 @@ const HANDLERS = {
|
|
|
979
1698
|
value: { total: result.length, shown: limit },
|
|
980
1699
|
enumerable: false, writable: true, configurable: true,
|
|
981
1700
|
});
|
|
1701
|
+
Object.defineProperty(sliced, 'filterInfo', {
|
|
1702
|
+
value: result.filterInfo,
|
|
1703
|
+
enumerable: false, writable: true, configurable: true,
|
|
1704
|
+
});
|
|
982
1705
|
result = sliced;
|
|
983
1706
|
}
|
|
984
1707
|
return { ok: true, result, note };
|
|
@@ -1016,6 +1739,27 @@ const HANDLERS = {
|
|
|
1016
1739
|
prefix: p.prefix || null,
|
|
1017
1740
|
showUncertain: !p.hideUncertain,
|
|
1018
1741
|
});
|
|
1742
|
+
if (p.framework != null && String(p.framework).trim() !== '') {
|
|
1743
|
+
const framework = String(p.framework).trim().toLowerCase();
|
|
1744
|
+
const known = new Set([
|
|
1745
|
+
'actix', 'aspnet', 'aspnet-minimal', 'axios', 'axum',
|
|
1746
|
+
'dotnet-httpclient', 'express', 'fastapi', 'fetch', 'flask',
|
|
1747
|
+
'go-http', 'jax-rs', 'nestjs', 'nextjs', 'requests',
|
|
1748
|
+
'reqwest', 'spring', 'spring-client', 'unknown-python',
|
|
1749
|
+
]);
|
|
1750
|
+
if (!known.has(framework)) {
|
|
1751
|
+
return {
|
|
1752
|
+
ok: false,
|
|
1753
|
+
error: `Invalid --framework value: "${p.framework}". Valid: ${[...known].join(', ')}.`,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
result.routes = result.routes.filter(route => route.framework === framework);
|
|
1757
|
+
result.requests = result.requests.filter(request => request.framework === framework);
|
|
1758
|
+
result.bridges = result.bridges.filter(bridge =>
|
|
1759
|
+
bridge.route.framework === framework || bridge.request.framework === framework);
|
|
1760
|
+
result.unmatchedRoutes = result.unmatchedRoutes.filter(route => route.framework === framework);
|
|
1761
|
+
result.unmatchedRequests = result.unmatchedRequests.filter(request => request.framework === framework);
|
|
1762
|
+
}
|
|
1019
1763
|
// Apply --file pattern as an additional filter on routes/requests
|
|
1020
1764
|
if (p.file) {
|
|
1021
1765
|
const sub = String(p.file);
|
|
@@ -1030,9 +1774,7 @@ const HANDLERS = {
|
|
|
1030
1774
|
// Apply --exclude patterns to route/request files (deadcode-style boundary matching)
|
|
1031
1775
|
const exclude = toExcludeArray(p.exclude);
|
|
1032
1776
|
if (exclude.length > 0) {
|
|
1033
|
-
const
|
|
1034
|
-
new RegExp('(^|[/._-])' + pat + 's?([/._-]|$)', 'i'));
|
|
1035
|
-
const matches = (file) => regexes.some(rx => rx.test(file));
|
|
1777
|
+
const matches = (file) => !index.matchesFilters(file, { exclude });
|
|
1036
1778
|
result.routes = result.routes.filter(r => !matches(r.file));
|
|
1037
1779
|
result.requests = result.requests.filter(r => !matches(r.file));
|
|
1038
1780
|
result.bridges = result.bridges.filter(b => !matches(b.route.file) && !matches(b.request.file));
|
|
@@ -1074,6 +1816,8 @@ const HANDLERS = {
|
|
|
1074
1816
|
// suppress the "Matched" section in unmatched-only mode.)
|
|
1075
1817
|
result._bridge = wantBridge;
|
|
1076
1818
|
result._unmatched = wantUnmatched;
|
|
1819
|
+
result._serverOnly = !!p.serverOnly;
|
|
1820
|
+
result._clientOnly = !!p.clientOnly;
|
|
1077
1821
|
return { ok: true, result, note };
|
|
1078
1822
|
},
|
|
1079
1823
|
|
|
@@ -1100,6 +1844,11 @@ const HANDLERS = {
|
|
|
1100
1844
|
|
|
1101
1845
|
const entries = [];
|
|
1102
1846
|
const notes = [];
|
|
1847
|
+
const maxLines = num(p.maxLines, null);
|
|
1848
|
+
if (p.maxLines != null &&
|
|
1849
|
+
(maxLines === null || !Number.isInteger(maxLines) || maxLines < 1)) {
|
|
1850
|
+
return { ok: false, error: '--max-lines must be a positive integer.' };
|
|
1851
|
+
}
|
|
1103
1852
|
|
|
1104
1853
|
for (const fnName of fnNames) {
|
|
1105
1854
|
// For comma-separated names, each may have Class.method syntax
|
|
@@ -1131,10 +1880,10 @@ const HANDLERS = {
|
|
|
1131
1880
|
{
|
|
1132
1881
|
const kind = classMatches[0].type;
|
|
1133
1882
|
const article = /^[aeiou]/.test(kind) ? 'an' : 'a';
|
|
1134
|
-
notes.push(`"${fnName}" is ${article} ${kind}, not a function. Use \`
|
|
1883
|
+
notes.push(`"${fnName}" is ${article} ${kind}, not a function. Use \`source ${fnName}\` to extract it.`);
|
|
1135
1884
|
}
|
|
1136
1885
|
} else if ((index.symbols.get(actualName) || []).some(s => s.type === 'macro')) {
|
|
1137
|
-
notes.push(`"${fnName}" is a macro.
|
|
1886
|
+
notes.push(`"${fnName}" is a macro. Use \`source <file>:<start>-<end>\` for its body.`);
|
|
1138
1887
|
} else {
|
|
1139
1888
|
notes.push(`Function "${fnName}" not found.`);
|
|
1140
1889
|
}
|
|
@@ -1155,8 +1904,14 @@ const HANDLERS = {
|
|
|
1155
1904
|
// in-file matches were silently dropped in three languages).
|
|
1156
1905
|
if (matches.length > 1 && p.all) {
|
|
1157
1906
|
for (const m of matches) {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1907
|
+
const fullCode = readAndExtract(m);
|
|
1908
|
+
const totalLines = m.endLine - m.startLine + 1;
|
|
1909
|
+
const truncated = !!maxLines && totalLines > maxLines;
|
|
1910
|
+
const code = truncated
|
|
1911
|
+
? fullCode.split('\n').slice(0, maxLines).join('\n')
|
|
1912
|
+
: fullCode;
|
|
1913
|
+
entries.push({ match: m, code, totalLines, truncated,
|
|
1914
|
+
shownLines: truncated ? maxLines : totalLines });
|
|
1160
1915
|
}
|
|
1161
1916
|
continue;
|
|
1162
1917
|
}
|
|
@@ -1172,8 +1927,14 @@ const HANDLERS = {
|
|
|
1172
1927
|
notes.push(`Found ${matches.length} ${what} for "${fnName}". Showing ${match.relativePath}:${match.startLine}. Also in: ${others}. ${disambiguationHint(matches, match, p.file)}`);
|
|
1173
1928
|
}
|
|
1174
1929
|
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1930
|
+
const fullCode = readAndExtract(match);
|
|
1931
|
+
const totalLines = match.endLine - match.startLine + 1;
|
|
1932
|
+
const truncated = !!maxLines && totalLines > maxLines;
|
|
1933
|
+
const code = truncated
|
|
1934
|
+
? fullCode.split('\n').slice(0, maxLines).join('\n')
|
|
1935
|
+
: fullCode;
|
|
1936
|
+
entries.push({ match, code, totalLines, truncated,
|
|
1937
|
+
shownLines: truncated ? maxLines : totalLines });
|
|
1177
1938
|
}
|
|
1178
1939
|
|
|
1179
1940
|
if (entries.length === 0 && notes.length > 0) {
|
|
@@ -1209,9 +1970,9 @@ const HANDLERS = {
|
|
|
1209
1970
|
const fnMatches = index.find(p.name, { file: p.file, skipCounts: true })
|
|
1210
1971
|
.filter(m => m.type === 'function' || (m.params !== undefined && !CLASS_KIND_TYPES.includes(m.type)));
|
|
1211
1972
|
if (fnMatches.length > 0) {
|
|
1212
|
-
return { ok: false, error: `Class "${p.name}" not found — "${p.name}" is a ${fnMatches[0].type}. Use \`
|
|
1973
|
+
return { ok: false, error: `Class "${p.name}" not found — "${p.name}" is a ${fnMatches[0].type}. Use \`source ${p.name}\` instead.` };
|
|
1213
1974
|
}
|
|
1214
|
-
return { ok: false, error:
|
|
1975
|
+
return { ok: false, error: symbolNotFound(index, p.name, 'Class') };
|
|
1215
1976
|
}
|
|
1216
1977
|
|
|
1217
1978
|
const entries = [];
|
|
@@ -1253,7 +2014,7 @@ const HANDLERS = {
|
|
|
1253
2014
|
|
|
1254
2015
|
// Large class summary mode (>200 lines, no maxLines)
|
|
1255
2016
|
if (totalLines > 200 && !maxLines) {
|
|
1256
|
-
const methods = index.findMethodsForType(match.name);
|
|
2017
|
+
const methods = index.findMethodsForType(match.name, match);
|
|
1257
2018
|
entries.push({ match, code: null, methods, totalLines, summaryMode: true, truncated: false });
|
|
1258
2019
|
return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
|
|
1259
2020
|
}
|
|
@@ -1301,7 +2062,7 @@ const HANDLERS = {
|
|
|
1301
2062
|
// reported "File not found" — factually wrong; imports/exporters
|
|
1302
2063
|
// list the candidates for the same input).
|
|
1303
2064
|
const resolved = index.resolveFilePathForQuery(p.file);
|
|
1304
|
-
const resolveErr = checkFileError(typeof resolved === 'string' ? null : resolved, p.file);
|
|
2065
|
+
const resolveErr = checkFileError(typeof resolved === 'string' ? null : resolved, p.file, index);
|
|
1305
2066
|
if (resolveErr) return { ok: false, error: resolveErr };
|
|
1306
2067
|
const filePath = resolved;
|
|
1307
2068
|
|
|
@@ -1342,7 +2103,7 @@ const HANDLERS = {
|
|
|
1342
2103
|
const err = requireFile(p.file);
|
|
1343
2104
|
if (err) return { ok: false, error: err };
|
|
1344
2105
|
const result = index.imports(p.file);
|
|
1345
|
-
const fileErr = checkFileError(result, p.file);
|
|
2106
|
+
const fileErr = checkFileError(result, p.file, index);
|
|
1346
2107
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1347
2108
|
return { ok: true, result };
|
|
1348
2109
|
},
|
|
@@ -1351,7 +2112,7 @@ const HANDLERS = {
|
|
|
1351
2112
|
const err = requireFile(p.file);
|
|
1352
2113
|
if (err) return { ok: false, error: err };
|
|
1353
2114
|
const result = index.exporters(p.file);
|
|
1354
|
-
const fileErr = checkFileError(result, p.file);
|
|
2115
|
+
const fileErr = checkFileError(result, p.file, index);
|
|
1355
2116
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1356
2117
|
return { ok: true, result };
|
|
1357
2118
|
},
|
|
@@ -1360,7 +2121,7 @@ const HANDLERS = {
|
|
|
1360
2121
|
const err = requireFile(p.file);
|
|
1361
2122
|
if (err) return { ok: false, error: err };
|
|
1362
2123
|
const result = index.fileExports(p.file);
|
|
1363
|
-
const fileErr = checkFileError(result, p.file);
|
|
2124
|
+
const fileErr = checkFileError(result, p.file, index);
|
|
1364
2125
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1365
2126
|
return { ok: true, result };
|
|
1366
2127
|
},
|
|
@@ -1375,7 +2136,7 @@ const HANDLERS = {
|
|
|
1375
2136
|
if (result && result.error === 'invalid-direction') {
|
|
1376
2137
|
return { ok: false, error: result.message };
|
|
1377
2138
|
}
|
|
1378
|
-
const fileErr = checkFileError(result, p.file);
|
|
2139
|
+
const fileErr = checkFileError(result, p.file, index);
|
|
1379
2140
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1380
2141
|
return { ok: true, result };
|
|
1381
2142
|
},
|
|
@@ -1418,7 +2179,7 @@ const HANDLERS = {
|
|
|
1418
2179
|
...(p.includeUncertain !== undefined && { includeUncertain: p.includeUncertain }),
|
|
1419
2180
|
});
|
|
1420
2181
|
if (result && result.found === false) {
|
|
1421
|
-
return { ok: false, error:
|
|
2182
|
+
return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
1422
2183
|
}
|
|
1423
2184
|
return { ok: true, result };
|
|
1424
2185
|
},
|
|
@@ -1447,8 +2208,9 @@ const HANDLERS = {
|
|
|
1447
2208
|
...(p.line && { line: p.line }),
|
|
1448
2209
|
});
|
|
1449
2210
|
if (result && result.found === false) {
|
|
1450
|
-
return { ok: false, error:
|
|
2211
|
+
return { ok: false, error: symbolNotFound(index, p.name, 'Function') };
|
|
1451
2212
|
}
|
|
2213
|
+
if (result && result.error) return { ok: false, error: result.error };
|
|
1452
2214
|
return { ok: true, result };
|
|
1453
2215
|
},
|
|
1454
2216
|
|
|
@@ -1460,9 +2222,21 @@ const HANDLERS = {
|
|
|
1460
2222
|
});
|
|
1461
2223
|
const limit = num(p.limit, undefined);
|
|
1462
2224
|
let note;
|
|
1463
|
-
if (limit && limit > 0 && result
|
|
1464
|
-
|
|
1465
|
-
|
|
2225
|
+
if (limit && limit > 0 && result) {
|
|
2226
|
+
const groups = ['functions', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
|
|
2227
|
+
const total = groups.reduce((sum, key) => sum + (result[key]?.length || 0), 0);
|
|
2228
|
+
if (total > limit) {
|
|
2229
|
+
let remaining = limit;
|
|
2230
|
+
const limited = { ...result };
|
|
2231
|
+
for (const key of groups) {
|
|
2232
|
+
const values = result[key] || [];
|
|
2233
|
+
limited[key] = values.slice(0, Math.max(0, remaining));
|
|
2234
|
+
remaining -= limited[key].length;
|
|
2235
|
+
}
|
|
2236
|
+
limited.limitInfo = { total, shown: limit };
|
|
2237
|
+
result = limited;
|
|
2238
|
+
note = limitNote(limit, total);
|
|
2239
|
+
}
|
|
1466
2240
|
}
|
|
1467
2241
|
return { ok: true, result, note };
|
|
1468
2242
|
},
|
|
@@ -1477,7 +2251,13 @@ const HANDLERS = {
|
|
|
1477
2251
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1478
2252
|
const classErr = validateClassName(index, p.name, p.className);
|
|
1479
2253
|
if (classErr) return { ok: false, error: classErr };
|
|
1480
|
-
const result = index.typedef(p.name, {
|
|
2254
|
+
const result = index.typedef(p.name, {
|
|
2255
|
+
exact: p.exact || false,
|
|
2256
|
+
className: p.className,
|
|
2257
|
+
file: p.file,
|
|
2258
|
+
exclude: applyTestExclusions(p.exclude, p.includeTests),
|
|
2259
|
+
in: p.in,
|
|
2260
|
+
});
|
|
1481
2261
|
return { ok: true, result };
|
|
1482
2262
|
},
|
|
1483
2263
|
|
|
@@ -1494,9 +2274,21 @@ const HANDLERS = {
|
|
|
1494
2274
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
1495
2275
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1496
2276
|
}
|
|
1497
|
-
|
|
2277
|
+
if (p.in) {
|
|
2278
|
+
let anyIn = false;
|
|
2279
|
+
for (const [, fileEntry] of index.files) {
|
|
2280
|
+
if (index.matchesFilters(fileEntry.relativePath, { in: p.in })) {
|
|
2281
|
+
anyIn = true;
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (!anyIn) {
|
|
2286
|
+
return { ok: false, error: `No files matched the 'in' directory filter '${p.in}'.` };
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
let result = index.api(p.file, { in: p.in });
|
|
1498
2290
|
if (p.file) {
|
|
1499
|
-
const fileErr = checkFileError(result, p.file);
|
|
2291
|
+
const fileErr = checkFileError(result, p.file, index);
|
|
1500
2292
|
if (fileErr) return { ok: false, error: fileErr };
|
|
1501
2293
|
}
|
|
1502
2294
|
// Apply limit to api results (api returns an array)
|
|
@@ -1504,6 +2296,7 @@ const HANDLERS = {
|
|
|
1504
2296
|
let note;
|
|
1505
2297
|
if (limit && limit > 0 && Array.isArray(result)) {
|
|
1506
2298
|
const { items, total, limited } = applyLimit(result, limit);
|
|
2299
|
+
const apiInfo = result.apiInfo;
|
|
1507
2300
|
if (limited) {
|
|
1508
2301
|
note = limitNote(limit, total);
|
|
1509
2302
|
// Full-set size travels with the payload for --json
|
|
@@ -1513,6 +2306,12 @@ const HANDLERS = {
|
|
|
1513
2306
|
enumerable: false, writable: true, configurable: true,
|
|
1514
2307
|
});
|
|
1515
2308
|
}
|
|
2309
|
+
if (apiInfo) {
|
|
2310
|
+
Object.defineProperty(items, 'apiInfo', {
|
|
2311
|
+
value: apiInfo,
|
|
2312
|
+
enumerable: false, writable: true, configurable: true,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
1516
2315
|
result = items;
|
|
1517
2316
|
}
|
|
1518
2317
|
return { ok: true, result, note };
|
|
@@ -1565,6 +2364,9 @@ const HANDLERS = {
|
|
|
1565
2364
|
functions: p.functions || false,
|
|
1566
2365
|
hot: p.hot || false,
|
|
1567
2366
|
top,
|
|
2367
|
+
file: p.file,
|
|
2368
|
+
in: p.in,
|
|
2369
|
+
exclude: toExcludeArray(p.exclude),
|
|
1568
2370
|
});
|
|
1569
2371
|
return note ? { ok: true, result, note } : { ok: true, result };
|
|
1570
2372
|
},
|
|
@@ -1601,7 +2403,7 @@ const HANDLERS = {
|
|
|
1601
2403
|
const scopeHint = p.symbolName ? ` (from context for "${p.symbolName}")` : '';
|
|
1602
2404
|
return { ok: false, error: `Item ${p.itemNum} not found${scopeHint}. Available: 1-${p.itemCount}` };
|
|
1603
2405
|
}
|
|
1604
|
-
return { ok: false, error: 'No expandable items. Run
|
|
2406
|
+
return { ok: false, error: 'No expandable items. Run show first.' };
|
|
1605
2407
|
}
|
|
1606
2408
|
const rendered = renderExpandItem(p.match, index.root, { validateRoot: p.validateRoot || false });
|
|
1607
2409
|
if (!rendered.ok) return { ok: false, error: rendered.error };
|
|
@@ -1634,6 +2436,10 @@ function execute(index, command, params = {}) {
|
|
|
1634
2436
|
// normalizers and returned wrong not-found answers).
|
|
1635
2437
|
params = { ...params };
|
|
1636
2438
|
try {
|
|
2439
|
+
if (PUBLIC_COMMAND_SET.has(command)) {
|
|
2440
|
+
const validationError = validatePublicParams(command, params);
|
|
2441
|
+
if (validationError) return { ok: false, error: validationError };
|
|
2442
|
+
}
|
|
1637
2443
|
// Resolve name-less handles (e.g. `lib.js:42`) via index lookup before dispatch.
|
|
1638
2444
|
// Handles WITH a name suffix are handled later by applyClassMethodSyntax.
|
|
1639
2445
|
if (params && params.name && looksLikeHandle(params.name)) {
|
|
@@ -1673,4 +2479,4 @@ function lookupByLocation(index, file, line) {
|
|
|
1673
2479
|
return null;
|
|
1674
2480
|
}
|
|
1675
2481
|
|
|
1676
|
-
module.exports = { execute };
|
|
2482
|
+
module.exports = { execute, validatePublicParams };
|