ucn 5.3.8 → 5.4.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 +41 -5
- package/.claude/skills/ucn/references/commands.md +8 -2
- package/cli/index.js +1 -0
- package/core/accessors.js +48 -1
- package/core/account.js +4 -0
- package/core/analysis.js +224 -10
- package/core/ast-analysis.js +96 -0
- package/core/bridge.js +11 -6
- package/core/cache.js +2 -1
- package/core/callers.js +21 -2
- package/core/check.js +35 -6
- package/core/command-contracts.js +1 -1
- package/core/execute.js +22 -3
- package/core/output/analysis-ext.js +26 -2
- package/core/output/analysis.js +11 -9
- package/core/output/check.js +6 -2
- package/core/output/endpoints.js +10 -7
- package/core/output/lines.js +25 -3
- package/core/output/public.js +13 -4
- package/core/output/refactoring.js +4 -1
- package/core/output/reporting.js +1 -1
- package/core/output-budget.js +8 -6
- package/core/project.js +5 -0
- package/core/registry.js +1 -1
- package/core/search.js +9 -1
- package/core/shared.js +13 -4
- package/core/verify.js +29 -6
- package/languages/index.js +4 -0
- package/languages/python.js +8 -1
- package/package.json +2 -2
package/core/ast-analysis.js
CHANGED
|
@@ -28,6 +28,101 @@ const CALLABLE_NODES = new Set([
|
|
|
28
28
|
'operator_declaration', 'conversion_operator_declaration',
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
|
+
const DECLARATION_NODES = {
|
|
32
|
+
class: new Set(['class_declaration', 'abstract_class_declaration', 'class_definition', 'class_specifier', 'class']),
|
|
33
|
+
struct: new Set(['struct_item', 'struct_specifier', 'struct_declaration', 'type_spec']),
|
|
34
|
+
interface: new Set(['interface_declaration', 'type_spec']),
|
|
35
|
+
type: new Set(['type_alias_declaration', 'type_definition', 'type_spec', 'type_alias', 'type_item', 'associated_type']),
|
|
36
|
+
enum: new Set(['enum_declaration', 'enum_item', 'enum_specifier']),
|
|
37
|
+
trait: new Set(['trait_item']),
|
|
38
|
+
impl: new Set(['impl_item']),
|
|
39
|
+
record: new Set(['record_declaration']),
|
|
40
|
+
field: new Set(['field_definition', 'public_field_definition', 'property_signature', 'field_declaration', 'variable_declarator']),
|
|
41
|
+
state: new Set(['variable_declarator', 'assignment', 'init_declarator', 'const_item', 'static_item', 'const_spec', 'var_spec']),
|
|
42
|
+
};
|
|
43
|
+
const FUNCTION_EXPRESSIONS = new Set([
|
|
44
|
+
'function_expression', 'generator_function', 'arrow_function', 'lambda',
|
|
45
|
+
'func_literal', 'closure_expression', 'lambda_expression', 'anonymous_method_expression',
|
|
46
|
+
]);
|
|
47
|
+
const COMMENT_NODES = new Set(['comment', 'line_comment', 'block_comment']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compare declarations by AST tokens, not lines: a class header and its first
|
|
51
|
+
* method can share a line. Concrete methods belong to the callable diff;
|
|
52
|
+
* bodyless signatures, fields (including unindexed Python assignments), and
|
|
53
|
+
* nested types belong to the declaration. Function-valued fields retain their
|
|
54
|
+
* signature but not their executable body. Nothing here is persisted in the
|
|
55
|
+
* index, so old and current source use the same projection without a cache bump.
|
|
56
|
+
* Missing AST mappings return no snapshot; callers keep conservative reporting.
|
|
57
|
+
*/
|
|
58
|
+
function declarationSnapshots(content, language, symbols) {
|
|
59
|
+
const snapshots = new Map();
|
|
60
|
+
if (content == null || symbols.length === 0) return snapshots;
|
|
61
|
+
const parser = getParser(language);
|
|
62
|
+
if (!parser) return snapshots;
|
|
63
|
+
const root = safeParse(parser, content).rootNode;
|
|
64
|
+
const wantedKinds = new Set(symbols.flatMap(s => [...(DECLARATION_NODES[s.type] || [])]));
|
|
65
|
+
const candidates = new Map();
|
|
66
|
+
walkNamed(root, node => {
|
|
67
|
+
if (!wantedKinds.has(node.type)) return;
|
|
68
|
+
if (!candidates.has(node.type)) candidates.set(node.type, []);
|
|
69
|
+
candidates.get(node.type).push(node);
|
|
70
|
+
});
|
|
71
|
+
const hasName = (node, name) => {
|
|
72
|
+
if (!node) return false;
|
|
73
|
+
if (node.text === name) return true;
|
|
74
|
+
return hasName(node.childForFieldName('name') || node.childForFieldName('declarator') ||
|
|
75
|
+
(node.type === 'generic_type' && node.childForFieldName('type')), name);
|
|
76
|
+
};
|
|
77
|
+
for (const symbol of symbols) {
|
|
78
|
+
const kinds = DECLARATION_NODES[symbol.type];
|
|
79
|
+
if (!kinds) continue;
|
|
80
|
+
let declaration = null;
|
|
81
|
+
for (const kind of kinds) {
|
|
82
|
+
for (const node of candidates.get(kind) || []) {
|
|
83
|
+
if (node.startPosition.row + 1 < symbol.startLine ||
|
|
84
|
+
node.endPosition.row + 1 > (symbol.endLine || symbol.startLine)) continue;
|
|
85
|
+
const name = node.childForFieldName('name') || node.childForFieldName('declarator') ||
|
|
86
|
+
node.childForFieldName('left') || (symbol.type === 'impl' && node.childForFieldName('type'));
|
|
87
|
+
if (!hasName(name, symbol.typeName || symbol.name)) continue;
|
|
88
|
+
if (!declaration || node.endIndex - node.startIndex > declaration.endIndex - declaration.startIndex) {
|
|
89
|
+
declaration = node;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!declaration || declaration.hasError) continue;
|
|
94
|
+
// Export modifiers and Python decorators live outside the declaration.
|
|
95
|
+
while (['export_statement', 'decorated_definition'].includes(declaration.parent?.type) &&
|
|
96
|
+
declaration.parent.startPosition.row + 1 >= symbol.startLine) declaration = declaration.parent;
|
|
97
|
+
const tokens = [];
|
|
98
|
+
const lines = new Set();
|
|
99
|
+
const visit = node => {
|
|
100
|
+
if (COMMENT_NODES.has(node.type) || node.type === 'pass_statement') return;
|
|
101
|
+
if (node.type === 'decorated_definition' &&
|
|
102
|
+
CALLABLE_NODES.has(node.childForFieldName('definition')?.type)) return;
|
|
103
|
+
// Class docstrings are documentation, not fields or inheritance.
|
|
104
|
+
if (language === 'python' && node.type === 'expression_statement' &&
|
|
105
|
+
node.namedChildCount === 1 && ['string', 'concatenated_string'].includes(node.namedChild(0).type)) return;
|
|
106
|
+
const body = CALLABLE_NODES.has(node.type) ? node.childForFieldName('body') : null;
|
|
107
|
+
if (body && !FUNCTION_EXPRESSIONS.has(node.type)) return;
|
|
108
|
+
if (node.childCount === 0) {
|
|
109
|
+
// Empty anonymous semicolons between members are separators.
|
|
110
|
+
if (node.type === ';' && ['class_body', 'interface_body', 'declaration_list'].includes(node.parent?.type)) return;
|
|
111
|
+
tokens.push([node.type, node.text]);
|
|
112
|
+
for (let line = node.startPosition.row + 1; line <= node.endPosition.row + 1; line++) lines.add(line);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for (const child of node.children) {
|
|
116
|
+
if (body && child.id === body.id) continue;
|
|
117
|
+
visit(child);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
visit(declaration);
|
|
121
|
+
snapshots.set(symbol, { signature: JSON.stringify(tokens), lines });
|
|
122
|
+
}
|
|
123
|
+
return snapshots;
|
|
124
|
+
}
|
|
125
|
+
|
|
31
126
|
const BRANCH_NODES = new Set([
|
|
32
127
|
'if_statement', 'if_expression', 'elif_clause',
|
|
33
128
|
'for_statement', 'for_in_statement', 'for_expression',
|
|
@@ -375,6 +470,7 @@ function projectComputedDispatch(index) {
|
|
|
375
470
|
}
|
|
376
471
|
|
|
377
472
|
module.exports = {
|
|
473
|
+
declarationSnapshots,
|
|
378
474
|
computeAstComplexity,
|
|
379
475
|
computedDispatchSites,
|
|
380
476
|
projectComputedDispatch,
|
package/core/bridge.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
'use strict';
|
|
27
27
|
|
|
28
28
|
const fs = require('fs');
|
|
29
|
-
const { codeUnitCompare } = require('./shared');
|
|
29
|
+
const { codeUnitCompare, isTestPath } = require('./shared');
|
|
30
30
|
const path = require('path');
|
|
31
31
|
const { getCachedCalls } = require('./callers');
|
|
32
32
|
const { getParser, safeParse } = require('../languages');
|
|
@@ -1490,9 +1490,12 @@ function endpoints(index, options = {}) {
|
|
|
1490
1490
|
showUncertain: options.showUncertain !== false,
|
|
1491
1491
|
};
|
|
1492
1492
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1493
|
+
const label = r => ({ ...r, isTest: isTestPath(r.file) });
|
|
1494
|
+
const inScope = r => index.matchesFilters(r.file, { in: options.in }) &&
|
|
1495
|
+
!(options.excludeTests && r.isTest);
|
|
1496
|
+
let routes = (opts.clientOnly ? [] : extractServerRoutes(index)).map(label).filter(inScope);
|
|
1497
|
+
let requests = (opts.serverOnly ? [] : extractClientRequests(index)).map(label).filter(inScope);
|
|
1498
|
+
let uncertainRequests = (opts.serverOnly ? [] : (index._endpointsCache?.uncertainRequests || [])).map(label).filter(inScope);
|
|
1496
1499
|
if (uncertainRequests.length > 0) {
|
|
1497
1500
|
// A server route registration (`@app.get("/x")`, `router.get("/x", h)`)
|
|
1498
1501
|
// is request-shaped too; the route inventory already owns those lines.
|
|
@@ -1514,12 +1517,14 @@ function endpoints(index, options = {}) {
|
|
|
1514
1517
|
uncertainRequests = uncertainRequests.filter(r => r.method.toUpperCase() === opts.method || r.method === 'request');
|
|
1515
1518
|
}
|
|
1516
1519
|
|
|
1517
|
-
let bridges = opts.bridge ? bridgeEndpoints(index)
|
|
1520
|
+
let bridges = opts.bridge ? bridgeEndpoints(index).map(b => ({
|
|
1521
|
+
...b, route: label(b.route), request: label(b.request),
|
|
1522
|
+
})) : [];
|
|
1518
1523
|
if (!opts.showUncertain) {
|
|
1519
1524
|
bridges = bridges.filter(b => b.matchType !== 'uncertain');
|
|
1520
1525
|
}
|
|
1521
1526
|
// If user filtered routes/requests, also constrain bridges
|
|
1522
|
-
if (opts.method || opts.prefix) {
|
|
1527
|
+
if (opts.method || opts.prefix || options.in || options.excludeTests) {
|
|
1523
1528
|
const routeKeys = new Set(routes.map(r => `${r.absoluteFile}:${r.line}:${r.method}:${r.path}`));
|
|
1524
1529
|
const reqKeys = new Set(requests.map(r => `${r.absoluteFile}:${r.line}:${r.method}:${r.path}`));
|
|
1525
1530
|
bridges = bridges.filter(b =>
|
package/core/cache.js
CHANGED
|
@@ -713,7 +713,8 @@ function clearAllCaches() {
|
|
|
713
713
|
// v225 (fix #357): Rust `use path::name as local` bindings record the original name with a paired `renames` alias.
|
|
714
714
|
// v226: bundled/minified filename exclusions are disclosed in discoveryIssues.
|
|
715
715
|
// v227: signature parameter/return text excludes AST comments in every language.
|
|
716
|
-
|
|
716
|
+
// v228: Python keyword arguments retain the same callable-reference facts as positional arguments.
|
|
717
|
+
const CACHE_FORMAT_VERSION = 228;
|
|
717
718
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
718
719
|
|
|
719
720
|
/**
|
package/core/callers.js
CHANGED
|
@@ -291,6 +291,22 @@ function _javaConstructorDisposition(index, filePath, fileEntry, call, targetDef
|
|
|
291
291
|
return 'unknown';
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// A structural member passed as a value is not invocation syntax. Keep it
|
|
295
|
+
// in the callback model only when a project member can actually be callable;
|
|
296
|
+
// an unrelated standalone function's spelling is not such evidence. Module
|
|
297
|
+
// members remain eligible because modules can export standalone functions.
|
|
298
|
+
function isDataMemberReference(fileEntry, call, definitions) {
|
|
299
|
+
if (!call.isFunctionReference || !call.isMethod || call.receiverIsModule ||
|
|
300
|
+
call.receiverModuleSpecifier || langTraits(fileEntry?.language)?.typeSystem !== 'structural') return false;
|
|
301
|
+
if (_structuralModuleBindings(fileEntry, call).length > 0) return false;
|
|
302
|
+
return !definitions.some(def => !NON_CALLABLE_TYPES.has(def.type) &&
|
|
303
|
+
// A typed Python descriptor read really invokes its getter; retain
|
|
304
|
+
// that existing proof path. An untyped property spelling belongs to
|
|
305
|
+
// the separate property-access inventory, never the call band.
|
|
306
|
+
(!require('./accessors').isAccessorDefinition(def) || call.receiverType) &&
|
|
307
|
+
(def.className || def.receiver));
|
|
308
|
+
}
|
|
309
|
+
|
|
294
310
|
/**
|
|
295
311
|
* Find all call sites that invoke the named symbol.
|
|
296
312
|
*
|
|
@@ -697,6 +713,7 @@ function findCallers(index, name, options = {}) {
|
|
|
697
713
|
langTraits(fileEntry.language)?.typeSystem === 'structural';
|
|
698
714
|
|
|
699
715
|
for (let call of calls) {
|
|
716
|
+
if (isDataMemberReference(fileEntry, call, options.targetDefinitions || definitions)) continue;
|
|
700
717
|
// fix #353: C# `Beta.Helper.Widget()` — the parser records a
|
|
701
718
|
// field hop rooted at `this` (Beta is no local). When the
|
|
702
719
|
// prefix names a project NAMESPACE that declares the last
|
|
@@ -5444,7 +5461,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5444
5461
|
// to this definition's source range. Nested closures deliberately
|
|
5445
5462
|
// remain in the slice and retain the existing inner-symbol rules.
|
|
5446
5463
|
const calls = _callsInDefinitionRange(index, def.file, allCalls,
|
|
5447
|
-
def.startLine, def.endLine)
|
|
5464
|
+
def.startLine, def.endLine).filter(call => !isDataMemberReference(
|
|
5465
|
+
index.files.get(def.file), call, index.symbols.get(call.name) || []));
|
|
5448
5466
|
// The reachability walk uses the legacy (non-accounting) path and most
|
|
5449
5467
|
// entry/test symbols contain no calls. Avoid constructing receiver,
|
|
5450
5468
|
// overload, and flow machinery for an empty source range. Contract
|
|
@@ -5545,7 +5563,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
5545
5563
|
if (!entry) {
|
|
5546
5564
|
const defs = index.symbols.get(call.name) || [];
|
|
5547
5565
|
const owners = defs.filter(s => !NON_CALLABLE_TYPES.has(s.type)).length;
|
|
5548
|
-
entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners,
|
|
5566
|
+
entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners,
|
|
5567
|
+
...(call.isFunctionReference && { functionReference: true }), ...meta };
|
|
5549
5568
|
unverifiedCallees.set(key, entry);
|
|
5550
5569
|
}
|
|
5551
5570
|
entry.callCount++;
|
package/core/check.js
CHANGED
|
@@ -73,6 +73,11 @@ function check(index, options = {}) {
|
|
|
73
73
|
const modified = (dr && Array.isArray(dr.functions)) ? dr.functions : [];
|
|
74
74
|
const added = (dr && Array.isArray(dr.newFunctions)) ? dr.newFunctions : [];
|
|
75
75
|
const deleted = (dr && Array.isArray(dr.deletedFunctions)) ? dr.deletedFunctions : [];
|
|
76
|
+
const declarations = [
|
|
77
|
+
...(dr?.symbols || []).map(s => ({ ...s, _kind: 'modified', _nonCallable: true })),
|
|
78
|
+
...(dr?.newSymbols || []).map(s => ({ ...s, _kind: 'added', _nonCallable: true })),
|
|
79
|
+
...(dr?.deletedSymbols || []).map(s => ({ ...s, _kind: 'deleted', _nonCallable: true })),
|
|
80
|
+
];
|
|
76
81
|
const pathCounts = {
|
|
77
82
|
changedPaths: dr?.changedPaths || 0,
|
|
78
83
|
nonSourcePaths: dr?.nonSourcePaths || 0,
|
|
@@ -82,9 +87,10 @@ function check(index, options = {}) {
|
|
|
82
87
|
const allChanged = [
|
|
83
88
|
...modified.map(f => ({ ...f, _kind: 'modified' })),
|
|
84
89
|
...added.map(f => ({ ...f, _kind: 'added' })),
|
|
90
|
+
...declarations,
|
|
85
91
|
];
|
|
86
92
|
|
|
87
|
-
if (!dr || (
|
|
93
|
+
if (!dr || (allChanged.length === 0 && deleted.length === 0)) {
|
|
88
94
|
// fix #283: say what the diff actually contained. "no changes
|
|
89
95
|
// detected" with three changed files reads as a false negative in a
|
|
90
96
|
// pre-commit hook — the truth is the changes are outside what the
|
|
@@ -95,7 +101,7 @@ function check(index, options = {}) {
|
|
|
95
101
|
if (changedPaths > 0 && nonSourcePaths === changedPaths) {
|
|
96
102
|
reason = `${changedPaths} changed path(s), all outside supported source files; untracked source files are included`;
|
|
97
103
|
} else if (changedPaths > 0) {
|
|
98
|
-
reason = 'no
|
|
104
|
+
reason = 'no indexed-symbol changes in the diff or untracked source files';
|
|
99
105
|
}
|
|
100
106
|
return {
|
|
101
107
|
base: options.base || 'HEAD',
|
|
@@ -116,9 +122,23 @@ function check(index, options = {}) {
|
|
|
116
122
|
// For each changed function, run verify and gather caller summary
|
|
117
123
|
for (const fn of changed) {
|
|
118
124
|
const filePath = fn.relativePath || fn.file || '';
|
|
125
|
+
if (fn._nonCallable) {
|
|
126
|
+
const dependency = fn.impact?.typeReferences || fn.impact?.propertyAccesses;
|
|
127
|
+
items.push({
|
|
128
|
+
name: fn.name, file: filePath, line: fn.startLine, kind: fn._kind,
|
|
129
|
+
symbolType: fn.type, requiresToolchainValidation: true,
|
|
130
|
+
callerCount: fn.impact?.totalCallSites || 0,
|
|
131
|
+
unverifiedCallerCount: fn.impact?.unverifiedSites?.length || 0,
|
|
132
|
+
dependencyCount: dependency?.confirmedCount || 0,
|
|
133
|
+
unverifiedDependencyCount: (dependency?.unverifiedCount || 0) + (fn.remainingReferences?.length || 0),
|
|
134
|
+
signatureMismatches: 0,
|
|
135
|
+
account: summarizeAccount(fn.impact?.account),
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
119
139
|
let verifyResult;
|
|
120
140
|
try {
|
|
121
|
-
verifyResult = index.verify(fn.name, { file: filePath });
|
|
141
|
+
verifyResult = index.verify(fn.name, { file: filePath, line: fn.startLine });
|
|
122
142
|
} catch (e) {
|
|
123
143
|
verifyResult = null;
|
|
124
144
|
}
|
|
@@ -225,6 +245,13 @@ function check(index, options = {}) {
|
|
|
225
245
|
// Action items
|
|
226
246
|
const actions = [];
|
|
227
247
|
for (const it of items) {
|
|
248
|
+
if (it.requiresToolchainValidation) {
|
|
249
|
+
actions.push({
|
|
250
|
+
severity: it.kind === 'added' ? 'warn' : 'error',
|
|
251
|
+
kind: 'declaration_change',
|
|
252
|
+
message: `${it.name}: ${it.kind} ${it.symbolType}; shape/inheritance compatibility is not checked by arity analysis — review dependency sites and run the compiler/type checker and tests`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
228
255
|
if (!it.account || !it.account.textComplete) {
|
|
229
256
|
actions.push({
|
|
230
257
|
severity: 'error',
|
|
@@ -272,7 +299,7 @@ function check(index, options = {}) {
|
|
|
272
299
|
actions.push({
|
|
273
300
|
severity: 'error',
|
|
274
301
|
kind: 'truncated_change_set',
|
|
275
|
-
message: `${allChanged.length - limit} changed
|
|
302
|
+
message: `${allChanged.length - limit} changed symbol(s) were not checked; rerun without --limit`,
|
|
276
303
|
});
|
|
277
304
|
}
|
|
278
305
|
if (testFiles.length > 0) {
|
|
@@ -289,9 +316,10 @@ function check(index, options = {}) {
|
|
|
289
316
|
const signatureMismatches = items.reduce((sum, it) => sum + (it.signatureMismatches || 0), 0);
|
|
290
317
|
const filteredEdges = items.reduce((sum, it) => sum + (it.account ? it.account.filtered || 0 : 0), 0);
|
|
291
318
|
const usageReviewSymbols = items.filter(it => it.account && it.account.requiresUsageReview).length;
|
|
319
|
+
const unvalidatedDeclarations = items.filter(it => it.requiresToolchainValidation && it.kind !== 'added').length;
|
|
292
320
|
const reviewRequired = incompleteAccounts > 0 || unverifiedCallSites > 0 ||
|
|
293
321
|
signatureMismatches > 0 || filteredEdges > 0 || usageReviewSymbols > 0 ||
|
|
294
|
-
actions.some(a => a.kind === 'orphan_new') ||
|
|
322
|
+
actions.some(a => a.kind === 'orphan_new' || a.kind === 'declaration_change') ||
|
|
295
323
|
!!(limit && allChanged.length > limit);
|
|
296
324
|
|
|
297
325
|
return {
|
|
@@ -308,7 +336,7 @@ function check(index, options = {}) {
|
|
|
308
336
|
totalTests: testCount,
|
|
309
337
|
actions,
|
|
310
338
|
trust: {
|
|
311
|
-
status: incompleteAccounts > 0 || signatureMismatches > 0
|
|
339
|
+
status: incompleteAccounts > 0 || signatureMismatches > 0 || unvalidatedDeclarations > 0
|
|
312
340
|
? 'BLOCKED'
|
|
313
341
|
: reviewRequired ? 'REVIEW_REQUIRED' : 'READY_FOR_TOOLCHAIN',
|
|
314
342
|
accountsChecked: items.filter(it => it.account && it.account.available).length,
|
|
@@ -317,6 +345,7 @@ function check(index, options = {}) {
|
|
|
317
345
|
signatureMismatches,
|
|
318
346
|
filteredEdges,
|
|
319
347
|
usageReviewSymbols,
|
|
348
|
+
unvalidatedDeclarations,
|
|
320
349
|
semanticComplete: false,
|
|
321
350
|
safeToDelete: false,
|
|
322
351
|
requiresCompilerAndTests: true,
|
|
@@ -301,7 +301,7 @@ const COMMAND_CONTRACTS = Object.freeze({
|
|
|
301
301
|
{ name: 'bridge', when: '`bridge=true`.', answer: 'Exact/parameterized route-request matches plus unmatched sides.' },
|
|
302
302
|
{ name: 'unmatched', when: '`unmatched=true`.', answer: 'Only unmatched supported boundaries.' },
|
|
303
303
|
],
|
|
304
|
-
defaults: ['Framework-specific static extraction.', 'Interpolated-path uncertainty remains visible unless hidden explicitly.'],
|
|
304
|
+
defaults: ['Framework-specific static extraction.', 'Test routes/requests are labeled; excludeTests=true hides them and in scopes a directory.', 'Interpolated-path uncertainty remains visible unless hidden explicitly.'],
|
|
305
305
|
truth: 'Findings are static matches for supported framework call/decorator shapes; bridge confidence is match quality, not runtime probability.',
|
|
306
306
|
nonGoals: ['Network discovery.', 'Frameworks not represented by an endpoint adapter.'],
|
|
307
307
|
invalidCombinations: ['`serverOnly` and `clientOnly` cannot both describe a useful result.'],
|
package/core/execute.js
CHANGED
|
@@ -1462,6 +1462,7 @@ const HANDLERS = {
|
|
|
1462
1462
|
unused: p.unused || false,
|
|
1463
1463
|
caseSensitive: p.caseSensitive || false,
|
|
1464
1464
|
exclude,
|
|
1465
|
+
testExclude: p.includeTests ? undefined : ['test files'],
|
|
1465
1466
|
in: p.in,
|
|
1466
1467
|
file: p.file,
|
|
1467
1468
|
top: topVal || (p.lines ? undefined : 50),
|
|
@@ -1470,13 +1471,15 @@ const HANDLERS = {
|
|
|
1470
1471
|
const unsupported = (!p.regex && (p.term || p.name))
|
|
1471
1472
|
? require('./account').scanUnsupportedFiles(index, p.term || p.name)
|
|
1472
1473
|
: null;
|
|
1473
|
-
let note
|
|
1474
|
+
let note = result.meta.filesSkipped > 0
|
|
1475
|
+
? `${result.meta.filesSkipped} test file(s) hidden by default (--include-tests).`
|
|
1476
|
+
: undefined;
|
|
1474
1477
|
if (unsupported?.lines > 0) {
|
|
1475
1478
|
Object.defineProperty(result, 'unsupportedMatches', {
|
|
1476
1479
|
value: unsupported,
|
|
1477
1480
|
enumerable: false, writable: true, configurable: true,
|
|
1478
1481
|
});
|
|
1479
|
-
note = `${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not structurally analyzed; verify with grep/ripgrep
|
|
1482
|
+
note = combineNotes([note, `${unsupported.lines} matching line(s) in ${unsupported.fileCount} unsupported-language file(s) were not structurally analyzed; verify with grep/ripgrep.`]);
|
|
1480
1483
|
}
|
|
1481
1484
|
return { ok: true, result, structural: true, note };
|
|
1482
1485
|
}
|
|
@@ -1793,6 +1796,8 @@ const HANDLERS = {
|
|
|
1793
1796
|
method: normMethod,
|
|
1794
1797
|
prefix: p.prefix || null,
|
|
1795
1798
|
showUncertain: !p.hideUncertain,
|
|
1799
|
+
in: p.in,
|
|
1800
|
+
excludeTests: !!p.excludeTests,
|
|
1796
1801
|
});
|
|
1797
1802
|
if (p.framework != null && String(p.framework).trim() !== '') {
|
|
1798
1803
|
const framework = String(p.framework).trim().toLowerCase();
|
|
@@ -1856,6 +1861,7 @@ const HANDLERS = {
|
|
|
1856
1861
|
// Recompute meta after filtering
|
|
1857
1862
|
result.meta = {
|
|
1858
1863
|
totalRoutes: result.routes.length,
|
|
1864
|
+
testRoutes: result.routes.filter(r => r.isTest).length,
|
|
1859
1865
|
totalRequests: result.requests.length,
|
|
1860
1866
|
uncertainRequests: (result.uncertainRequests || []).length,
|
|
1861
1867
|
totalBridges: result.bridges.length,
|
|
@@ -2283,7 +2289,7 @@ const HANDLERS = {
|
|
|
2283
2289
|
const limit = num(p.limit, undefined);
|
|
2284
2290
|
let note;
|
|
2285
2291
|
if (limit && limit > 0 && result) {
|
|
2286
|
-
const groups = ['functions', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
|
|
2292
|
+
const groups = ['functions', 'symbols', 'newSymbols', 'deletedSymbols', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
|
|
2287
2293
|
const total = groups.reduce((sum, key) => sum + (result[key]?.length || 0), 0);
|
|
2288
2294
|
if (total > limit) {
|
|
2289
2295
|
let remaining = limit;
|
|
@@ -2506,6 +2512,18 @@ function execute(index, command, params = {}) {
|
|
|
2506
2512
|
const validationError = validatePublicParams(command, params);
|
|
2507
2513
|
if (validationError) return { ok: false, error: validationError };
|
|
2508
2514
|
}
|
|
2515
|
+
// Public JSON paths and pasted stack frames may be absolute. Resolve
|
|
2516
|
+
// only indexed files, then use the same relative scope as our handles.
|
|
2517
|
+
const relativeIndexedFile = file => {
|
|
2518
|
+
if (!file || !path.isAbsolute(file)) return file;
|
|
2519
|
+
const resolved = index.resolveFilePathForQuery(file);
|
|
2520
|
+
return typeof resolved === 'string' ? index.files.get(resolved).relativePath : file;
|
|
2521
|
+
};
|
|
2522
|
+
if (params.file) params.file = relativeIndexedFile(params.file);
|
|
2523
|
+
const absoluteHandle = params.name && parseSymbolHandle(params.name);
|
|
2524
|
+
if (absoluteHandle && path.isAbsolute(absoluteHandle.file)) {
|
|
2525
|
+
params.name = relativeIndexedFile(absoluteHandle.file) + params.name.slice(absoluteHandle.file.length);
|
|
2526
|
+
}
|
|
2509
2527
|
// Resolve name-less handles (e.g. `lib.js:42`) via index lookup before dispatch.
|
|
2510
2528
|
// Handles WITH a name suffix are handled later by applyClassMethodSyntax.
|
|
2511
2529
|
if (params && params.name && looksLikeHandle(params.name)) {
|
|
@@ -2520,6 +2538,7 @@ function execute(index, command, params = {}) {
|
|
|
2520
2538
|
}
|
|
2521
2539
|
}
|
|
2522
2540
|
const response = handler(index, params);
|
|
2541
|
+
response.projectRoot = index.root;
|
|
2523
2542
|
const bundled = (index.discoveryIssues || []).filter(issue => issue.reason === 'bundled');
|
|
2524
2543
|
if (bundled.length > 0) {
|
|
2525
2544
|
const files = bundled.slice(0, 5).map(issue => issue.relativePath).join(', ');
|
|
@@ -207,8 +207,14 @@ function formatDiffImpact(result, options = {}) {
|
|
|
207
207
|
if (s.modifiedFunctions > 0) parts.push(`${s.modifiedFunctions} modified`);
|
|
208
208
|
if (s.deletedFunctions > 0) parts.push(`${s.deletedFunctions} deleted`);
|
|
209
209
|
if (s.newFunctions > 0) parts.push(`${s.newFunctions} new`);
|
|
210
|
+
if (s.modifiedSymbols || s.newSymbols || s.deletedSymbols) {
|
|
211
|
+
parts.push(`${s.modifiedSymbols || 0} modified, ${s.newSymbols || 0} new, ${s.deletedSymbols || 0} deleted non-callable declarations`);
|
|
212
|
+
}
|
|
210
213
|
parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
|
|
211
214
|
if (s.unverifiedCallSites > 0) parts.push(`${s.unverifiedCallSites} unverified`);
|
|
215
|
+
if (s.totalDependencySites || s.unverifiedDependencySites) {
|
|
216
|
+
parts.push(`${s.totalDependencySites || 0} confirmed + ${s.unverifiedDependencySites || 0} unverified non-call dependency sites across ${s.dependencyFiles || 0} files`);
|
|
217
|
+
}
|
|
212
218
|
lines.push(parts.join(', '));
|
|
213
219
|
// fix #283: changed paths outside supported source are invisible to the
|
|
214
220
|
// symbol analysis — disclose instead of silently narrowing the diff.
|
|
@@ -229,10 +235,10 @@ function formatDiffImpact(result, options = {}) {
|
|
|
229
235
|
lines.push(` ${fn.relativePath}:${fn.startLine}`);
|
|
230
236
|
lines.push(` ${fn.signature}`);
|
|
231
237
|
if (fn.addedLines.length > 0) {
|
|
232
|
-
lines.push(`
|
|
238
|
+
lines.push(` Added at lines: ${formatLineRanges(fn.addedLines)}`);
|
|
233
239
|
}
|
|
234
240
|
if (fn.deletedLines.length > 0) {
|
|
235
|
-
lines.push(`
|
|
241
|
+
lines.push(` Deleted at old lines: ${formatLineRanges(fn.deletedLines)}`);
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
if (fn.callers.length > 0) {
|
|
@@ -302,6 +308,24 @@ function formatDiffImpact(result, options = {}) {
|
|
|
302
308
|
}
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
for (const [key, title] of [['symbols', 'MODIFIED'], ['newSymbols', 'NEW'], ['deletedSymbols', 'DELETED']]) {
|
|
312
|
+
if (!result[key]?.length) continue;
|
|
313
|
+
lines.push(`\n${title} DECLARATIONS:`);
|
|
314
|
+
for (const symbol of result[key]) {
|
|
315
|
+
lines.push(` ${symbol.type} ${symbol.name} — ${symbol.relativePath}:${symbol.startLine}`);
|
|
316
|
+
if (symbol.impact) {
|
|
317
|
+
lines.push(require('./analysis').formatImpact(symbol.impact, { compact: true }));
|
|
318
|
+
} else {
|
|
319
|
+
const refs = symbol.remainingReferences || [];
|
|
320
|
+
lines.push(` Remaining name references: ${refs.length} unverified (deleted target)`);
|
|
321
|
+
for (const site of refs.slice(0, MAX_CALLERS_PER_FN)) {
|
|
322
|
+
lines.push(` ${site.file}:${site.line}: ${site.expression}`);
|
|
323
|
+
}
|
|
324
|
+
if (refs.length > MAX_CALLERS_PER_FN) lines.push(` (+${refs.length - MAX_CALLERS_PER_FN} more)`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
305
329
|
// Module-level changes
|
|
306
330
|
if (result.moduleLevelChanges.length > 0) {
|
|
307
331
|
lines.push('\nMODULE-LEVEL CHANGES:');
|
package/core/output/analysis.js
CHANGED
|
@@ -205,7 +205,7 @@ function formatCalleeAccountLine(acct) {
|
|
|
205
205
|
function unverifiedCalleeLines(entries, compact) {
|
|
206
206
|
if (!entries || entries.length === 0) return [];
|
|
207
207
|
const lines = [];
|
|
208
|
-
lines.push(`${compact ? '' : '\n'}CALLEES — UNVERIFIED (${entries.length}) — call syntax, receiver/binding unresolved:`);
|
|
208
|
+
lines.push(`${compact ? '' : '\n'}CALLEES — UNVERIFIED (${entries.length}) — call or callable-reference syntax, receiver/binding unresolved:`);
|
|
209
209
|
for (const u of entries) {
|
|
210
210
|
const owners = u.ownerCount > 1 ? ` (${u.ownerCount} owners)` : '';
|
|
211
211
|
const sites = u.sites && u.sites.length > 0 ? ` L${u.sites.join(',L')}` : '';
|
|
@@ -461,7 +461,7 @@ function formatContext(ctx, options = {}) {
|
|
|
461
461
|
|
|
462
462
|
const typeUnverified = ctx.unverifiedCallers || [];
|
|
463
463
|
if (typeUnverified.length > 0) {
|
|
464
|
-
lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
464
|
+
lines.push(`\nCALLERS — UNVERIFIED (${typeUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
465
465
|
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
466
466
|
const cap = 10;
|
|
467
467
|
let shown = 0;
|
|
@@ -688,7 +688,7 @@ function formatContext(ctx, options = {}) {
|
|
|
688
688
|
// Actionable ambiguity: call syntax without enough identity evidence.
|
|
689
689
|
// Always visible and capped at 10 one-liners unless --all.
|
|
690
690
|
if (actionableUnverified.length > 0) {
|
|
691
|
-
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
691
|
+
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
692
692
|
formatAmbiguityCandidates(lines, ctx.ambiguityCandidates);
|
|
693
693
|
const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
|
|
694
694
|
let shown = 0;
|
|
@@ -837,8 +837,10 @@ function formatImpact(impact, options = {}) {
|
|
|
837
837
|
}
|
|
838
838
|
|
|
839
839
|
// By file (confirmed tier)
|
|
840
|
-
if (
|
|
841
|
-
|
|
840
|
+
if (impact.byFile.length > 0) {
|
|
841
|
+
if (!compact) lines.push('');
|
|
842
|
+
lines.push('BY FILE:');
|
|
843
|
+
}
|
|
842
844
|
|
|
843
845
|
// Evidence aggregate over ALL sites (replaces per-edge confidence lines)
|
|
844
846
|
const allSites = impact.byFile.flatMap(g => g.sites);
|
|
@@ -881,7 +883,7 @@ function formatImpact(impact, options = {}) {
|
|
|
881
883
|
for (const site of group.sites) {
|
|
882
884
|
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
883
885
|
const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
884
|
-
lines.push(` ${group.file}:${site.line}${caller}${expr}`);
|
|
886
|
+
lines.push(` ${group.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}`);
|
|
885
887
|
}
|
|
886
888
|
}
|
|
887
889
|
if (access.unverifiedSites.length > 0) {
|
|
@@ -889,7 +891,7 @@ function formatImpact(impact, options = {}) {
|
|
|
889
891
|
for (const site of access.unverifiedSites.slice(0, 10)) {
|
|
890
892
|
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
891
893
|
const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
892
|
-
lines.push(` ${site.file}:${site.line}${caller}${expr}`);
|
|
894
|
+
lines.push(` ${site.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}`);
|
|
893
895
|
}
|
|
894
896
|
if (access.unverifiedSites.length > 10) {
|
|
895
897
|
lines.push(` (+${access.unverifiedSites.length - 10} more unverified)`);
|
|
@@ -927,7 +929,7 @@ function formatImpact(impact, options = {}) {
|
|
|
927
929
|
|
|
928
930
|
// Unverified tier: visible, capped at 10 one-liners
|
|
929
931
|
if (impactUnverified.length > 0) {
|
|
930
|
-
lines.push(`${compact ? '' : '\n'}UNVERIFIED CALL SITES (${impactUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
932
|
+
lines.push(`${compact ? '' : '\n'}UNVERIFIED CALL SITES (${impactUnverified.length}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
931
933
|
const cap = 10;
|
|
932
934
|
for (const site of impactUnverified.slice(0, cap)) {
|
|
933
935
|
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
@@ -1085,7 +1087,7 @@ function formatAbout(about, options = {}) {
|
|
|
1085
1087
|
const aboutUnverified = about.callers.unverified;
|
|
1086
1088
|
if (aboutUnverified && aboutUnverified.total > 0) {
|
|
1087
1089
|
lines.push('');
|
|
1088
|
-
lines.push(`CALLERS — UNVERIFIED (${aboutUnverified.total}) — call syntax, no binding/receiver evidence:`);
|
|
1090
|
+
lines.push(`CALLERS — UNVERIFIED (${aboutUnverified.total}) — call or callable-reference syntax, no binding/receiver evidence:`);
|
|
1089
1091
|
for (const u of aboutUnverified.top) {
|
|
1090
1092
|
const caller = u.callerName ? ` [${u.callerName}]` : '';
|
|
1091
1093
|
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
package/core/output/check.js
CHANGED
|
@@ -29,15 +29,17 @@ function formatCheck(result) {
|
|
|
29
29
|
if (result.trust.signatureMismatches) trustDetails.push(`${result.trust.signatureMismatches} signature mismatch(es)`);
|
|
30
30
|
if (result.trust.filteredEdges) trustDetails.push(`${result.trust.filteredEdges} filtered edge(s)`);
|
|
31
31
|
if (result.trust.usageReviewSymbols) trustDetails.push(`${result.trust.usageReviewSymbols} symbol(s) need usages review`);
|
|
32
|
+
if (result.trust.unvalidatedDeclarations) trustDetails.push(`${result.trust.unvalidatedDeclarations} declaration change(s) need toolchain validation`);
|
|
32
33
|
if (trustDetails.length > 0) lines.push(` ${trustDetails.join(' · ')}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// Changed functions section
|
|
36
37
|
const items = result.changed || [];
|
|
38
|
+
const noun = items.some(it => it.symbolType) ? 'symbol' : 'function';
|
|
37
39
|
if (result.truncated) {
|
|
38
|
-
lines.push(`Changed: ${items.length} of ${result.totalChanged}
|
|
40
|
+
lines.push(`Changed: ${items.length} of ${result.totalChanged} ${noun}s`);
|
|
39
41
|
} else {
|
|
40
|
-
lines.push(`Changed: ${items.length}
|
|
42
|
+
lines.push(`Changed: ${items.length} ${noun}${items.length === 1 ? '' : 's'}`);
|
|
41
43
|
}
|
|
42
44
|
if (items.length === 0) {
|
|
43
45
|
lines.push(' (none — only non-function changes)');
|
|
@@ -47,6 +49,7 @@ function formatCheck(result) {
|
|
|
47
49
|
if (it.kind && it.kind !== 'changed') tags.push(it.kind.toUpperCase());
|
|
48
50
|
if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
|
|
49
51
|
if (it.orphan) tags.push('ORPHAN');
|
|
52
|
+
if (it.symbolType) tags.push(it.symbolType);
|
|
50
53
|
if (it.account && !it.account.textComplete) tags.push('ACCOUNT-INCOMPLETE');
|
|
51
54
|
const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
|
|
52
55
|
let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
@@ -54,6 +57,7 @@ function formatCheck(result) {
|
|
|
54
57
|
callers += ` (+${it.unverifiedCallerCount} unverified)`;
|
|
55
58
|
}
|
|
56
59
|
lines.push(` ${it.name} (${it.file}:${it.line})${tagStr} ${callers}`);
|
|
60
|
+
if (it.symbolType) lines.push(` Dependencies: ${it.dependencyCount || 0} confirmed, ${it.unverifiedDependencyCount || 0} unverified`);
|
|
57
61
|
if (it.mismatches && it.mismatches.length > 0) {
|
|
58
62
|
for (const m of it.mismatches.slice(0, 3)) {
|
|
59
63
|
const where = m.file ? ` at ${m.file}:${m.line}` : '';
|