ucn 5.4.0 → 5.4.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 +14 -4
- package/.claude/skills/ucn/references/commands.md +3 -3
- package/cli/index.js +3 -2
- package/core/accessors.js +48 -1
- package/core/analysis.js +144 -9
- package/core/ast-analysis.js +96 -0
- package/core/bridge.js +11 -6
- package/core/check.js +35 -6
- package/core/command-contracts.js +1 -1
- package/core/execute.js +4 -1
- package/core/output/analysis-ext.js +30 -6
- package/core/output/analysis.js +6 -4
- package/core/output/check.js +6 -2
- package/core/output/endpoints.js +10 -7
- package/core/output/find.js +4 -0
- package/core/output/lines.js +33 -5
- package/core/output/reporting.js +1 -1
- package/core/output-budget.js +30 -17
- package/core/registry.js +1 -1
- package/core/verify.js +23 -5
- package/mcp/server.js +3 -3
- package/package.json +2 -2
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
|
@@ -1796,6 +1796,8 @@ const HANDLERS = {
|
|
|
1796
1796
|
method: normMethod,
|
|
1797
1797
|
prefix: p.prefix || null,
|
|
1798
1798
|
showUncertain: !p.hideUncertain,
|
|
1799
|
+
in: p.in,
|
|
1800
|
+
excludeTests: !!p.excludeTests,
|
|
1799
1801
|
});
|
|
1800
1802
|
if (p.framework != null && String(p.framework).trim() !== '') {
|
|
1801
1803
|
const framework = String(p.framework).trim().toLowerCase();
|
|
@@ -1859,6 +1861,7 @@ const HANDLERS = {
|
|
|
1859
1861
|
// Recompute meta after filtering
|
|
1860
1862
|
result.meta = {
|
|
1861
1863
|
totalRoutes: result.routes.length,
|
|
1864
|
+
testRoutes: result.routes.filter(r => r.isTest).length,
|
|
1862
1865
|
totalRequests: result.requests.length,
|
|
1863
1866
|
uncertainRequests: (result.uncertainRequests || []).length,
|
|
1864
1867
|
totalBridges: result.bridges.length,
|
|
@@ -2286,7 +2289,7 @@ const HANDLERS = {
|
|
|
2286
2289
|
const limit = num(p.limit, undefined);
|
|
2287
2290
|
let note;
|
|
2288
2291
|
if (limit && limit > 0 && result) {
|
|
2289
|
-
const groups = ['functions', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
|
|
2292
|
+
const groups = ['functions', 'symbols', 'newSymbols', 'deletedSymbols', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
|
|
2290
2293
|
const total = groups.reduce((sum, key) => sum + (result[key]?.length || 0), 0);
|
|
2291
2294
|
if (total > limit) {
|
|
2292
2295
|
let remaining = limit;
|
|
@@ -204,12 +204,18 @@ function formatDiffImpact(result, options = {}) {
|
|
|
204
204
|
|
|
205
205
|
const s = result.summary || {};
|
|
206
206
|
const parts = [];
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
for (const [label, suffix] of [['Functions', 'Functions'], ['Declarations', 'Symbols']]) {
|
|
208
|
+
const counts = ['modified', 'new', 'deleted']
|
|
209
|
+
.filter(kind => s[kind + suffix] > 0)
|
|
210
|
+
.map(kind => `${s[kind + suffix]} ${kind}`);
|
|
211
|
+
if (counts.length) parts.push(`${label}: ${counts.join(', ')}`);
|
|
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`);
|
|
212
|
-
|
|
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
|
+
}
|
|
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.
|
|
215
221
|
if (result.nonSourcePaths > 0) {
|
|
@@ -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
|
@@ -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)`);
|
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}` : '';
|
package/core/output/endpoints.js
CHANGED
|
@@ -55,7 +55,7 @@ function formatUncertainRequests(result, options) {
|
|
|
55
55
|
lines.push(`Possible client requests (${list.length}) — request-shaped call with a path literal, receiver not recognized as an HTTP client:`);
|
|
56
56
|
const cap = options.all ? Infinity : 10;
|
|
57
57
|
for (const r of list.slice(0, cap)) {
|
|
58
|
-
lines.push(` ${r.file}:${r.line} ${r.receiver}.${r.method}(${JSON.stringify(r.path)}) in ${r.callerName}`);
|
|
58
|
+
lines.push(` ${r.file}:${r.line} ${r.receiver}.${r.method}(${JSON.stringify(r.path)}) in ${r.callerName}${r.isTest ? ' [test]' : ''}`);
|
|
59
59
|
}
|
|
60
60
|
if (list.length > cap) lines.push(` (+${list.length - cap} more — use --all)`);
|
|
61
61
|
return lines;
|
|
@@ -88,7 +88,7 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
|
|
|
88
88
|
for (const r of list) {
|
|
89
89
|
const handler = r.handler || '<anonymous>';
|
|
90
90
|
const fw = r.framework ? `[${r.framework}]` : '';
|
|
91
|
-
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw} :${r.line}`);
|
|
91
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw}${r.isTest ? ' [test]' : ''} :${r.line}`);
|
|
92
92
|
}
|
|
93
93
|
lines.push('');
|
|
94
94
|
}
|
|
@@ -115,7 +115,7 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
|
|
|
115
115
|
const inferred = r.methodInferred ? '?' : '';
|
|
116
116
|
const interp = r.interp ? ' (interp)' : '';
|
|
117
117
|
const fw = r.framework ? `[${r.framework}]` : '';
|
|
118
|
-
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} ${fw} :${r.line}`);
|
|
118
|
+
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} ${fw}${r.isTest ? ' [test]' : ''} :${r.line}`);
|
|
119
119
|
}
|
|
120
120
|
lines.push('');
|
|
121
121
|
}
|
|
@@ -166,12 +166,12 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
166
166
|
|
|
167
167
|
lines.push(`Matched (${sorted.length} routes):`);
|
|
168
168
|
for (const { route, clients } of sorted) {
|
|
169
|
-
lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}] ${route.file}:${route.line}`);
|
|
169
|
+
lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}]${route.isTest ? ' [test]' : ''} ${route.file}:${route.line}`);
|
|
170
170
|
for (const b of clients) {
|
|
171
171
|
const conf = b.confidence.toFixed(2);
|
|
172
172
|
const tier = b.matchType.toUpperCase();
|
|
173
173
|
const inf = b.methodInferred ? ' method?' : '';
|
|
174
|
-
lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf}) from ${b.request.callerName} ${b.request.file}:${b.request.line}`);
|
|
174
|
+
lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf}) from ${b.request.callerName}${b.request.isTest ? ' [test]' : ''} ${b.request.file}:${b.request.line}`);
|
|
175
175
|
}
|
|
176
176
|
lines.push('');
|
|
177
177
|
}
|
|
@@ -180,7 +180,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
180
180
|
if (unmatchedRoutes.length > 0) {
|
|
181
181
|
lines.push(`Unmatched server routes (${unmatchedRoutes.length}):`);
|
|
182
182
|
for (const r of unmatchedRoutes) {
|
|
183
|
-
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}] ${r.file}:${r.line}`);
|
|
183
|
+
lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}]${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
|
|
184
184
|
}
|
|
185
185
|
lines.push('');
|
|
186
186
|
}
|
|
@@ -190,7 +190,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
|
|
|
190
190
|
for (const r of unmatchedRequests) {
|
|
191
191
|
const inferred = r.methodInferred ? '?' : '';
|
|
192
192
|
const interp = r.interp ? ' (interp)' : '';
|
|
193
|
-
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} [${r.framework}] ${r.file}:${r.line}`);
|
|
193
|
+
lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} [${r.framework}]${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -213,6 +213,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
213
213
|
file: r.file,
|
|
214
214
|
line: r.line,
|
|
215
215
|
framework: r.framework,
|
|
216
|
+
isTest: !!r.isTest,
|
|
216
217
|
...(r.classPrefix && { classPrefix: r.classPrefix }),
|
|
217
218
|
});
|
|
218
219
|
const trimReq = (r) => ({
|
|
@@ -225,6 +226,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
225
226
|
callerName: r.callerName,
|
|
226
227
|
...(r.callerStartLine && { callerStartLine: r.callerStartLine }),
|
|
227
228
|
framework: r.framework,
|
|
229
|
+
isTest: !!r.isTest,
|
|
228
230
|
...(r.methodInferred && { methodInferred: true }),
|
|
229
231
|
});
|
|
230
232
|
const trimBridge = (b) => ({
|
|
@@ -250,6 +252,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
250
252
|
uncertainRequests: (result.uncertainRequests || []).map(r => ({
|
|
251
253
|
receiver: r.receiver, method: r.method, path: r.path,
|
|
252
254
|
file: r.file, line: r.line, callerName: r.callerName, reason: r.reason,
|
|
255
|
+
isTest: !!r.isTest,
|
|
253
256
|
})),
|
|
254
257
|
// In unmatched-only mode, the matched bridges array is suppressed
|
|
255
258
|
// — consumers that want both should not pass --unmatched.
|
package/core/output/find.js
CHANGED
|
@@ -158,10 +158,13 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
158
158
|
const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
|
|
159
159
|
const handle = formatSymbolHandle(s);
|
|
160
160
|
const loc = handle || (s.relativePath + ':' + s.startLine);
|
|
161
|
+
const nameLocation = s.nameLine && s.nameLine !== s.startLine
|
|
162
|
+
? `name token at ${s.relativePath || s.file}:${s.nameLine}` : '';
|
|
161
163
|
|
|
162
164
|
if (compact) {
|
|
163
165
|
// One line per result: "<handle> <sig> <usages?> <doc snippet?>"
|
|
164
166
|
const parts = [`${loc} ${sig}${confStr}`];
|
|
167
|
+
if (nameLocation) parts.push(`[${nameLocation}]`);
|
|
165
168
|
if (s.usageCounts !== undefined && s.usageCounts.total > 0) {
|
|
166
169
|
const scope = sameNameDefinitionCounts.get(s.name) > 1 ? ' name-wide' : '';
|
|
167
170
|
const label = s.usageCounts.complete === false
|
|
@@ -186,6 +189,7 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
lines.push(`${loc} ${sig}${confStr}`);
|
|
192
|
+
if (nameLocation) lines.push(` ${nameLocation} (handle starts at declaration line ${s.startLine})`);
|
|
189
193
|
if (s.docstring) {
|
|
190
194
|
const snip = firstSentenceShort(s.docstring);
|
|
191
195
|
if (snip) lines.push(` "${snip}"`);
|
package/core/output/lines.js
CHANGED
|
@@ -64,22 +64,39 @@ function accountComments(account) {
|
|
|
64
64
|
|
|
65
65
|
function findRecords(result) {
|
|
66
66
|
const out = [];
|
|
67
|
+
const notes = [];
|
|
67
68
|
if (Array.isArray(result)) {
|
|
68
|
-
for (const symbol of result)
|
|
69
|
+
for (const symbol of result) {
|
|
70
|
+
out.push(record(pathOf(symbol), symbol.startLine, signatureOf(symbol), symbol.type));
|
|
71
|
+
if (symbol.nameLine && symbol.nameLine !== symbol.startLine) {
|
|
72
|
+
notes.push(`# ${record(pathOf(symbol), symbol.startLine, symbol.name)} starts at the declaration; name token at line ${symbol.nameLine} (usages reports the token line).`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
69
75
|
} else if (result && Array.isArray(result.types)) {
|
|
70
76
|
for (const type of result.types) {
|
|
71
77
|
out.push(record(pathOf(type), type.startLine ?? type.line, type.name, type.type || type.kind));
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
|
-
return { records: out, notes
|
|
80
|
+
return { records: out, notes };
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
function usagesRecords(result) {
|
|
78
84
|
const out = [];
|
|
79
85
|
const notes = [];
|
|
86
|
+
const byLine = new Map();
|
|
80
87
|
for (const usage of Array.isArray(result) ? result : []) {
|
|
81
88
|
const kind = usage.isDefinition ? 'definition' : (usage.usageType || 'reference');
|
|
82
|
-
|
|
89
|
+
const key = `${pathOf(usage)}\0${usage.line}`;
|
|
90
|
+
if (!byLine.has(key)) byLine.set(key, { usage, kinds: new Set(), count: 0 });
|
|
91
|
+
const row = byLine.get(key);
|
|
92
|
+
row.kinds.add(kind);
|
|
93
|
+
row.count++;
|
|
94
|
+
}
|
|
95
|
+
for (const { usage, kinds, count } of byLine.values()) {
|
|
96
|
+
const tags = [...kinds].filter(kind => kind !== 'call');
|
|
97
|
+
if (kinds.has('call') && tags.length) tags.unshift('call');
|
|
98
|
+
if (count > 1) tags.push(`${count} occurrences`);
|
|
99
|
+
out.push(record(pathOf(usage), usage.line, usage.content, tags.join('; ')));
|
|
83
100
|
}
|
|
84
101
|
const counts = result && result.summaryCounts;
|
|
85
102
|
if (counts && counts.hiddenTestUsages > 0) {
|
|
@@ -187,6 +204,17 @@ function impactRecords(result) {
|
|
|
187
204
|
out.push(record(pathOf(site), site.line, site.content, 'unverified: deleted-target-name-match'));
|
|
188
205
|
}
|
|
189
206
|
}
|
|
207
|
+
for (const symbol of [...(result.symbols || []), ...(result.newSymbols || []), ...(result.deletedSymbols || [])]) {
|
|
208
|
+
out.push(record(symbol.relativePath, symbol.startLine, symbol.name, `${symbol.type} declaration change`));
|
|
209
|
+
if (symbol.impact) {
|
|
210
|
+
const nested = impactRecords(symbol.impact);
|
|
211
|
+
out.push(...nested.records);
|
|
212
|
+
notes.push(...nested.notes);
|
|
213
|
+
}
|
|
214
|
+
for (const site of symbol.remainingReferences || []) {
|
|
215
|
+
out.push(record(site.file, site.line, site.expression, 'unverified: deleted-target-name-match'));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
190
218
|
const summary = result.summary || {};
|
|
191
219
|
notes.push(`# Diff: ${summary.modifiedFunctions || 0} modified, ${summary.newFunctions || 0} new, ${summary.deletedFunctions || 0} deleted functions; ${(result.moduleLevelChanges || []).length} file(s) with module-level changes.`);
|
|
192
220
|
if (result.nonSourcePaths) notes.push(`# ${result.nonSourcePaths} changed path(s) outside supported source files not analyzed.`);
|
|
@@ -206,11 +234,11 @@ function impactRecords(result) {
|
|
|
206
234
|
const accesses = result.propertyAccesses;
|
|
207
235
|
for (const group of accesses.byFile || []) {
|
|
208
236
|
for (const access of group.sites || []) {
|
|
209
|
-
out.push(record(group.file, access.line, access.expression,
|
|
237
|
+
out.push(record(group.file, access.line, access.expression, `property-access: ${access.accessKind || 'access'}${Number.isInteger(access.column) ? `, column ${access.column + 1}` : ''}`));
|
|
210
238
|
}
|
|
211
239
|
}
|
|
212
240
|
for (const access of accesses.unverifiedSites || []) {
|
|
213
|
-
out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access`));
|
|
241
|
+
out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access: ${access.accessKind || 'access'}${Number.isInteger(access.column) ? `, column ${access.column + 1}` : ''}`));
|
|
214
242
|
}
|
|
215
243
|
notes.push(`# PROPERTY ACCESS SITES: ${accesses.confirmedCount} confirmed, ${accesses.unverifiedCount} unverified, ${accesses.excluded?.total || 0} other-target (separate from caller ACCOUNT).`);
|
|
216
244
|
}
|
package/core/output/reporting.js
CHANGED
|
@@ -284,7 +284,7 @@ function formatDeadcode(results, options = {}) {
|
|
|
284
284
|
lines.push(`\n${extHint}`);
|
|
285
285
|
}
|
|
286
286
|
if (results.excludedRuntimeContract > 0) {
|
|
287
|
-
lines.push(`\n${results.excludedRuntimeContract}
|
|
287
|
+
lines.push(`\n${results.excludedRuntimeContract} runtime callback(s) hidden (language/runtime registrations, not dead).`);
|
|
288
288
|
}
|
|
289
289
|
if (results.pythonImplicitExportFiles > 0) {
|
|
290
290
|
lines.push(`\nPython public-surface rule active in ${results.pythonImplicitExportFiles} file(s) without __all__: top-level non-underscore names are treated as externally reachable.`);
|
package/core/output-budget.js
CHANGED
|
@@ -15,7 +15,7 @@ const BROAD_COMMANDS = new Set([
|
|
|
15
15
|
...[...BROAD_CANONICAL].map(toMcpName),
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):|\d+ test-file usage\(s\) hidden\b|
|
|
18
|
+
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
|
|
19
19
|
const MAX_PRESERVED_CONTRACT_LINES = 24;
|
|
20
20
|
const MAX_PRESERVED_CONTRACT_CHARS = 8000;
|
|
21
21
|
|
|
@@ -132,29 +132,31 @@ function applyOutputBudget(text, {
|
|
|
132
132
|
all = false,
|
|
133
133
|
surface = 'cli',
|
|
134
134
|
params = {},
|
|
135
|
+
trailingChars = 0,
|
|
135
136
|
} = {}) {
|
|
137
|
+
const defaultLimit = BROAD_COMMANDS.has(command)
|
|
138
|
+
? BROAD_OUTPUT_CHARS
|
|
139
|
+
: DEFAULT_OUTPUT_CHARS;
|
|
140
|
+
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
141
|
+
const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
142
|
+
const limit = Math.max(0, hardLimit - trailingChars);
|
|
136
143
|
if (!text) {
|
|
137
144
|
return {
|
|
138
|
-
text: '(no output)',
|
|
139
|
-
truncated:
|
|
145
|
+
text: '(no output)'.slice(0, limit),
|
|
146
|
+
truncated: '(no output)'.length > limit,
|
|
140
147
|
fullChars: 0,
|
|
141
|
-
requestedLimit:
|
|
148
|
+
requestedLimit: hardLimit,
|
|
142
149
|
contractMetadata: [],
|
|
143
150
|
contractMetadataComplete: true,
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
const defaultLimit = BROAD_COMMANDS.has(command)
|
|
148
|
-
? BROAD_OUTPUT_CHARS
|
|
149
|
-
: DEFAULT_OUTPUT_CHARS;
|
|
150
|
-
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
151
|
-
const limit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
152
154
|
if (text.length <= limit) {
|
|
153
155
|
return {
|
|
154
156
|
text,
|
|
155
157
|
truncated: false,
|
|
156
158
|
fullChars: text.length,
|
|
157
|
-
requestedLimit:
|
|
159
|
+
requestedLimit: hardLimit,
|
|
158
160
|
contractMetadata: [],
|
|
159
161
|
contractMetadataComplete: true,
|
|
160
162
|
};
|
|
@@ -178,8 +180,8 @@ function applyOutputBudget(text, {
|
|
|
178
180
|
? `Raise ${raiseHint}.`
|
|
179
181
|
: `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
|
|
180
182
|
let notice = compactBudget
|
|
181
|
-
? `... OUTPUT TRUNCATED (${text.length}→${
|
|
182
|
-
: `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${
|
|
183
|
+
? `... OUTPUT TRUNCATED (${text.length}→${hardLimit}). ${compactGuidance}`
|
|
184
|
+
: `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${hardLimit}. ` +
|
|
183
185
|
`${narrowingHint(command, surface, params)} ${allHint}`;
|
|
184
186
|
if (compactBudget && notice.length > limit) {
|
|
185
187
|
const emergency = supportsAll
|
|
@@ -194,10 +196,21 @@ function applyOutputBudget(text, {
|
|
|
194
196
|
// can consume most of the transport. Trust/account lines take precedence
|
|
195
197
|
// over body detail and are appended directly after a compact notice.
|
|
196
198
|
if (compactBudget) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
maxChars: metadataCapacity,
|
|
199
|
+
let candidate = preservedContractMetadata(text, '', {
|
|
200
|
+
maxChars: Math.max(0, limit - notice.length - 1),
|
|
200
201
|
});
|
|
202
|
+
if (candidate.omitted > 0) {
|
|
203
|
+
// Spend less on generic guidance when that lets a complete scope
|
|
204
|
+
// or parameter warning survive even a tiny transport ceiling.
|
|
205
|
+
const shorterNotice = `... OUTPUT TRUNCATED. Raise ${raiseHint}.`;
|
|
206
|
+
const shorterCandidate = preservedContractMetadata(text, '', {
|
|
207
|
+
maxChars: Math.max(0, limit - shorterNotice.length - 1),
|
|
208
|
+
});
|
|
209
|
+
if (shorterNotice.length < notice.length && shorterCandidate.lines.length > candidate.lines.length) {
|
|
210
|
+
notice = shorterNotice;
|
|
211
|
+
candidate = shorterCandidate;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
201
214
|
const candidateText = candidate.lines.join('\n');
|
|
202
215
|
const separatorChars = candidateText ? 2 : 1;
|
|
203
216
|
const bodyBudget = Math.max(0,
|
|
@@ -225,7 +238,7 @@ function applyOutputBudget(text, {
|
|
|
225
238
|
text: pieces.join('\n').slice(0, limit),
|
|
226
239
|
truncated: true,
|
|
227
240
|
fullChars: text.length,
|
|
228
|
-
requestedLimit:
|
|
241
|
+
requestedLimit: hardLimit,
|
|
229
242
|
contractMetadata: contractMetadata.lines,
|
|
230
243
|
contractMetadataComplete: contractMetadata.complete,
|
|
231
244
|
};
|
|
@@ -283,7 +296,7 @@ function applyOutputBudget(text, {
|
|
|
283
296
|
text: rendered,
|
|
284
297
|
truncated: true,
|
|
285
298
|
fullChars: text.length,
|
|
286
|
-
requestedLimit:
|
|
299
|
+
requestedLimit: hardLimit,
|
|
287
300
|
contractMetadata: contractMetadata.lines,
|
|
288
301
|
contractMetadataComplete: contractMetadata.complete,
|
|
289
302
|
};
|
package/core/registry.js
CHANGED
|
@@ -149,7 +149,7 @@ const FLAG_APPLICABILITY = {
|
|
|
149
149
|
repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
|
|
150
150
|
deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
|
|
151
151
|
entrypoints: ['file', 'exclude', 'includeTests', 'excludeTests', 'limit', 'type', 'framework'],
|
|
152
|
-
endpoints: ['file', 'exclude', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
|
|
152
|
+
endpoints: ['file', 'in', 'exclude', 'excludeTests', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
|
|
153
153
|
stacktrace: ['stack'],
|
|
154
154
|
auditAsync: ['file', 'exclude', 'limit'],
|
|
155
155
|
};
|