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/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/lines.js
CHANGED
|
@@ -77,9 +77,20 @@ function findRecords(result) {
|
|
|
77
77
|
function usagesRecords(result) {
|
|
78
78
|
const out = [];
|
|
79
79
|
const notes = [];
|
|
80
|
+
const byLine = new Map();
|
|
80
81
|
for (const usage of Array.isArray(result) ? result : []) {
|
|
81
82
|
const kind = usage.isDefinition ? 'definition' : (usage.usageType || 'reference');
|
|
82
|
-
|
|
83
|
+
const key = `${pathOf(usage)}\0${usage.line}`;
|
|
84
|
+
if (!byLine.has(key)) byLine.set(key, { usage, kinds: new Set(), count: 0 });
|
|
85
|
+
const row = byLine.get(key);
|
|
86
|
+
row.kinds.add(kind);
|
|
87
|
+
row.count++;
|
|
88
|
+
}
|
|
89
|
+
for (const { usage, kinds, count } of byLine.values()) {
|
|
90
|
+
const tags = [...kinds].filter(kind => kind !== 'call');
|
|
91
|
+
if (kinds.has('call') && tags.length) tags.unshift('call');
|
|
92
|
+
if (count > 1) tags.push(`${count} occurrences`);
|
|
93
|
+
out.push(record(pathOf(usage), usage.line, usage.content, tags.join('; ')));
|
|
83
94
|
}
|
|
84
95
|
const counts = result && result.summaryCounts;
|
|
85
96
|
if (counts && counts.hiddenTestUsages > 0) {
|
|
@@ -187,6 +198,17 @@ function impactRecords(result) {
|
|
|
187
198
|
out.push(record(pathOf(site), site.line, site.content, 'unverified: deleted-target-name-match'));
|
|
188
199
|
}
|
|
189
200
|
}
|
|
201
|
+
for (const symbol of [...(result.symbols || []), ...(result.newSymbols || []), ...(result.deletedSymbols || [])]) {
|
|
202
|
+
out.push(record(symbol.relativePath, symbol.startLine, symbol.name, `${symbol.type} declaration change`));
|
|
203
|
+
if (symbol.impact) {
|
|
204
|
+
const nested = impactRecords(symbol.impact);
|
|
205
|
+
out.push(...nested.records);
|
|
206
|
+
notes.push(...nested.notes);
|
|
207
|
+
}
|
|
208
|
+
for (const site of symbol.remainingReferences || []) {
|
|
209
|
+
out.push(record(site.file, site.line, site.expression, 'unverified: deleted-target-name-match'));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
190
212
|
const summary = result.summary || {};
|
|
191
213
|
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
214
|
if (result.nonSourcePaths) notes.push(`# ${result.nonSourcePaths} changed path(s) outside supported source files not analyzed.`);
|
|
@@ -206,11 +228,11 @@ function impactRecords(result) {
|
|
|
206
228
|
const accesses = result.propertyAccesses;
|
|
207
229
|
for (const group of accesses.byFile || []) {
|
|
208
230
|
for (const access of group.sites || []) {
|
|
209
|
-
out.push(record(group.file, access.line, access.expression,
|
|
231
|
+
out.push(record(group.file, access.line, access.expression, `property-access: ${access.accessKind || 'access'}${Number.isInteger(access.column) ? `, column ${access.column + 1}` : ''}`));
|
|
210
232
|
}
|
|
211
233
|
}
|
|
212
234
|
for (const access of accesses.unverifiedSites || []) {
|
|
213
|
-
out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access`));
|
|
235
|
+
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
236
|
}
|
|
215
237
|
notes.push(`# PROPERTY ACCESS SITES: ${accesses.confirmedCount} confirmed, ${accesses.unverifiedCount} unverified, ${accesses.excluded?.total || 0} other-target (separate from caller ACCOUNT).`);
|
|
216
238
|
}
|
package/core/output/public.js
CHANGED
|
@@ -40,12 +40,20 @@ function appendNote(text, note) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** Canonicalize object keys so JSON bytes do not depend on index provenance. */
|
|
43
|
-
function canonicalJsonValue(value) {
|
|
44
|
-
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
43
|
+
function canonicalJsonValue(value, root, field = undefined) {
|
|
44
|
+
if (Array.isArray(value)) return value.map(item => canonicalJsonValue(item, root, field));
|
|
45
|
+
if (root && typeof value === 'string' &&
|
|
46
|
+
['file', 'filePath', 'callerFile', 'definitionFile', 'resolved', 'path', 'targetFile', 'from', 'to', 'root', 'files'].includes(field)) {
|
|
47
|
+
const path = require('path');
|
|
48
|
+
if (path.isAbsolute(value)) {
|
|
49
|
+
const relative = path.relative(root, value);
|
|
50
|
+
if (relative && relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)) return relative;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
45
53
|
if (!value || typeof value !== 'object') return value;
|
|
46
54
|
const canonical = {};
|
|
47
55
|
for (const key of Object.keys(value).sort()) {
|
|
48
|
-
canonical[key] = canonicalJsonValue(provenanceReplacer(key, value[key]));
|
|
56
|
+
canonical[key] = canonicalJsonValue(provenanceReplacer(key, value[key]), root, key);
|
|
49
57
|
}
|
|
50
58
|
return canonical;
|
|
51
59
|
}
|
|
@@ -474,12 +482,13 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
474
482
|
...(data && data.ok === false && { ok: false }),
|
|
475
483
|
...(modeOf(command, result) && { mode: modeOf(command, result) }),
|
|
476
484
|
contract: contractMeta(command),
|
|
485
|
+
...(execution.projectRoot && { pathBase: execution.projectRoot }),
|
|
477
486
|
...commandMeta,
|
|
478
487
|
...(execution.note && { note: execution.note }),
|
|
479
488
|
},
|
|
480
489
|
data,
|
|
481
490
|
};
|
|
482
|
-
return JSON.stringify(canonicalJsonValue(envelope), null, 2);
|
|
491
|
+
return JSON.stringify(canonicalJsonValue(envelope, execution.projectRoot), null, 2);
|
|
483
492
|
}
|
|
484
493
|
|
|
485
494
|
module.exports = {
|
|
@@ -522,7 +522,10 @@ function formatAuditAsync(result) {
|
|
|
522
522
|
lines.push(`${file} (${fileIssues.length})`);
|
|
523
523
|
for (const issue of fileIssues) {
|
|
524
524
|
const caller = issue.callerName ? ` [${issue.callerName}]` : '';
|
|
525
|
-
|
|
525
|
+
const detail = issue.reason === 'stored-promise-used-as-value'
|
|
526
|
+
? `${issue.variable} used as a resolved value; promise from ${issue.calleeName}() at line ${issue.originLine}`
|
|
527
|
+
: `${issue.calleeName}() — async, not awaited`;
|
|
528
|
+
lines.push(` :${issue.line}${caller} ${detail}`);
|
|
526
529
|
}
|
|
527
530
|
}
|
|
528
531
|
return lines.join('\n');
|
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
|
@@ -132,6 +132,7 @@ function applyOutputBudget(text, {
|
|
|
132
132
|
all = false,
|
|
133
133
|
surface = 'cli',
|
|
134
134
|
params = {},
|
|
135
|
+
trailingChars = 0,
|
|
135
136
|
} = {}) {
|
|
136
137
|
if (!text) {
|
|
137
138
|
return {
|
|
@@ -148,13 +149,14 @@ function applyOutputBudget(text, {
|
|
|
148
149
|
? BROAD_OUTPUT_CHARS
|
|
149
150
|
: DEFAULT_OUTPUT_CHARS;
|
|
150
151
|
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
151
|
-
const
|
|
152
|
+
const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
153
|
+
const limit = Math.max(0, hardLimit - trailingChars);
|
|
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
|
|
@@ -225,7 +227,7 @@ function applyOutputBudget(text, {
|
|
|
225
227
|
text: pieces.join('\n').slice(0, limit),
|
|
226
228
|
truncated: true,
|
|
227
229
|
fullChars: text.length,
|
|
228
|
-
requestedLimit:
|
|
230
|
+
requestedLimit: hardLimit,
|
|
229
231
|
contractMetadata: contractMetadata.lines,
|
|
230
232
|
contractMetadataComplete: contractMetadata.complete,
|
|
231
233
|
};
|
|
@@ -283,7 +285,7 @@ function applyOutputBudget(text, {
|
|
|
283
285
|
text: rendered,
|
|
284
286
|
truncated: true,
|
|
285
287
|
fullChars: text.length,
|
|
286
|
-
requestedLimit:
|
|
288
|
+
requestedLimit: hardLimit,
|
|
287
289
|
contractMetadata: contractMetadata.lines,
|
|
288
290
|
contractMetadataComplete: contractMetadata.complete,
|
|
289
291
|
};
|
package/core/project.js
CHANGED
|
@@ -1105,6 +1105,11 @@ class ProjectIndex {
|
|
|
1105
1105
|
const lowerPath = filePath.toLowerCase();
|
|
1106
1106
|
for (const pattern of filters.exclude) {
|
|
1107
1107
|
const lowerPattern = pattern.toLowerCase();
|
|
1108
|
+
if (lowerPattern === 'test files') {
|
|
1109
|
+
const rp = path.isAbsolute(filePath) ? path.relative(this.root, filePath) : filePath;
|
|
1110
|
+
if (require('./shared').isTestPath(rp)) return false;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1108
1113
|
let regex = this._excludeRegexCache?.get(lowerPattern);
|
|
1109
1114
|
if (!regex) {
|
|
1110
1115
|
const escaped = lowerPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
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
|
};
|
package/core/search.js
CHANGED
|
@@ -727,6 +727,7 @@ function structuralSearch(index, options = {}) {
|
|
|
727
727
|
// Auto-infer type: --receiver implies type=call
|
|
728
728
|
const type = options.type || (receiver ? 'call' : undefined);
|
|
729
729
|
const results = [];
|
|
730
|
+
const skippedTestFiles = new Set();
|
|
730
731
|
|
|
731
732
|
// Validate type if provided
|
|
732
733
|
if (type && !STRUCTURAL_TYPES.has(type)) {
|
|
@@ -753,7 +754,13 @@ function structuralSearch(index, options = {}) {
|
|
|
753
754
|
if (!rp.includes(options.file) && !rp.endsWith(options.file)) return false;
|
|
754
755
|
}
|
|
755
756
|
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
756
|
-
if (!index.matchesFilters(fileEntry.relativePath, {
|
|
757
|
+
if (!index.matchesFilters(fileEntry.relativePath, { in: options.in })) return false;
|
|
758
|
+
if (!index.matchesFilters(fileEntry.relativePath, { exclude: options.exclude })) {
|
|
759
|
+
if (options.testExclude && !index.matchesFilters(fileEntry.relativePath, { exclude: options.testExclude })) {
|
|
760
|
+
skippedTestFiles.add(fileEntry.relativePath);
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
757
764
|
}
|
|
758
765
|
return true;
|
|
759
766
|
};
|
|
@@ -978,6 +985,7 @@ function structuralSearch(index, options = {}) {
|
|
|
978
985
|
}).filter(([, v]) => v !== undefined && v !== null)),
|
|
979
986
|
totalMatched: total,
|
|
980
987
|
shown: results.length,
|
|
988
|
+
filesSkipped: skippedTestFiles.size,
|
|
981
989
|
...(unused && {
|
|
982
990
|
unusedScope: 'callable-symbols-only',
|
|
983
991
|
unusedSafety: 'candidate-only; use deadcode before deletion',
|
package/core/shared.js
CHANGED
|
@@ -50,15 +50,22 @@ function codeUnitCompare(a, b) {
|
|
|
50
50
|
* Path-based test heuristic — matches the same patterns as `find`'s exclusion
|
|
51
51
|
* logic so that `about` and `find` agree on which files are de-emphasized.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Uses language-specific filename conventions plus `test|tests|__tests__|__mocks__|fixture|mock`
|
|
54
|
+
* as path segments (with word boundaries on both sides). `spec` is a test
|
|
55
|
+
* marker only for languages whose test-file conventions include it.
|
|
55
56
|
*
|
|
56
57
|
* Complement to `isTestFile` (filename pattern check) — together they catch
|
|
57
58
|
* both `foo.test.js` (filename) AND `test/agent-benchmark.js` (directory).
|
|
58
59
|
*/
|
|
59
60
|
function isTestPath(rp) {
|
|
60
61
|
if (!rp) return false;
|
|
61
|
-
|
|
62
|
+
const language = detectLanguage(rp);
|
|
63
|
+
const { langTraits } = require('../languages');
|
|
64
|
+
const specConvention = langTraits(language)?.testFileCandidates?.('sample', '')
|
|
65
|
+
.some(candidate => candidate.includes('.spec'));
|
|
66
|
+
return isTestFile(rp, language) ||
|
|
67
|
+
/(^|[/._-])(test|tests|__tests__|__mocks__|fixture|mock)s?([/._-]|$)/i.test(rp) ||
|
|
68
|
+
(!!specConvention && /(^|[/._-])specs?([/._-]|$)/i.test(rp));
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
@@ -109,7 +116,9 @@ function pickBestDefinition(matches, opts = {}) {
|
|
|
109
116
|
* Returns a new array with test patterns appended (deduplicating).
|
|
110
117
|
*/
|
|
111
118
|
function addTestExclusions(exclude) {
|
|
112
|
-
|
|
119
|
+
// Keep automatic test classification distinct from explicit substring
|
|
120
|
+
// exclusions: --exclude=spec deliberately excludes Python specs too.
|
|
121
|
+
const testPatterns = ['test files'];
|
|
113
122
|
const existing = new Set((exclude || []).map(e => e.toLowerCase()));
|
|
114
123
|
const additions = testPatterns.filter(p => !existing.has(p));
|
|
115
124
|
return [...(exclude || []), ...additions];
|
package/core/verify.js
CHANGED
|
@@ -247,14 +247,26 @@ function classifyCallContext(callNode, language) {
|
|
|
247
247
|
/**
|
|
248
248
|
* Find a call expression node at the target line matching funcName
|
|
249
249
|
*/
|
|
250
|
-
function findCallNode(node, callTypes, targetRow, funcName, occurrence = 0) {
|
|
250
|
+
function findCallNode(node, callTypes, targetRow, funcName, occurrence = 0, site = null) {
|
|
251
251
|
// Several same-name calls can share one line (`greet("a") + greet("b")`,
|
|
252
252
|
// f-strings) — fix #231: callers pass the site's per-line ordinal so each
|
|
253
253
|
// record is arg-checked against ITS OWN node, not the line's first.
|
|
254
254
|
// Records and this walk are both pre-order, so ordinals align; an
|
|
255
255
|
// out-of-range ordinal falls back to the first match (never worse than
|
|
256
256
|
// the pre-fix behavior when a parse shape hides a node).
|
|
257
|
-
const matches = _collectCallNodes(node, callTypes, targetRow, funcName, occurrence + 1);
|
|
257
|
+
const matches = _collectCallNodes(node, callTypes, targetRow, funcName, site ? Infinity : occurrence + 1);
|
|
258
|
+
if (Number.isInteger(site?.start)) {
|
|
259
|
+
// The resolver already identified the callee token. Preserve that
|
|
260
|
+
// identity through argument extraction, including aliases and several
|
|
261
|
+
// calls on one line (some of which may target another declaration).
|
|
262
|
+
const exact = matches.filter(candidate => {
|
|
263
|
+
const callee = candidate.childForFieldName('function') ||
|
|
264
|
+
candidate.childForFieldName('name') ||
|
|
265
|
+
candidate.childForFieldName('constructor') || candidate.childForFieldName('type');
|
|
266
|
+
return callee && callee.startIndex <= site.start && callee.endIndex >= site.end;
|
|
267
|
+
});
|
|
268
|
+
if (exact.length === 1) return exact[0];
|
|
269
|
+
}
|
|
258
270
|
return matches[occurrence] || matches[0] || null;
|
|
259
271
|
}
|
|
260
272
|
|
|
@@ -795,8 +807,10 @@ function computePlanCallSites(index, name, def) {
|
|
|
795
807
|
content: c.content,
|
|
796
808
|
usageType: 'call',
|
|
797
809
|
receiver: c.receiver,
|
|
810
|
+
calledAs: c.calledAs,
|
|
811
|
+
callSite: c.provenance?.facts?.site,
|
|
798
812
|
};
|
|
799
|
-
const siteKey = `${c.file}:${c.line}`;
|
|
813
|
+
const siteKey = `${c.file}:${c.line}:${c.calledAs || name}`;
|
|
800
814
|
const occurrence = planLineSeen.get(siteKey) || 0;
|
|
801
815
|
planLineSeen.set(siteKey, occurrence + 1);
|
|
802
816
|
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
@@ -808,6 +822,8 @@ function computePlanCallSites(index, name, def) {
|
|
|
808
822
|
expression: (call.content || '').trim(),
|
|
809
823
|
args: analysis.args,
|
|
810
824
|
argCount: analysis.argCount,
|
|
825
|
+
...(analysis.keywordArgNames && { keywordArgNames: analysis.keywordArgNames }),
|
|
826
|
+
...(analysis.positionalCount != null && { positionalCount: analysis.positionalCount }),
|
|
811
827
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
812
828
|
});
|
|
813
829
|
}
|
|
@@ -896,7 +912,9 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
896
912
|
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
897
913
|
|
|
898
914
|
// Find the call expression at the target line matching funcName
|
|
899
|
-
const
|
|
915
|
+
const spelling = call.calledAs && call.calledAs !== 'bound' ? call.calledAs : funcName;
|
|
916
|
+
const callNode = findCallNode(tree.rootNode, callTypes, targetRow, spelling, occurrence,
|
|
917
|
+
call.callSite || call.provenance?.facts?.site);
|
|
900
918
|
if (!callNode) return { args: null, argCount: 0 };
|
|
901
919
|
|
|
902
920
|
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
@@ -1351,6 +1369,8 @@ function verify(index, name, options = {}) {
|
|
|
1351
1369
|
content: c.content,
|
|
1352
1370
|
usageType: 'call',
|
|
1353
1371
|
receiver: c.receiver,
|
|
1372
|
+
calledAs: c.calledAs,
|
|
1373
|
+
callSite: c.provenance?.facts?.site,
|
|
1354
1374
|
// Preserve receiver identity through the usage-shaped adapter. Go
|
|
1355
1375
|
// permits a local value to have the same spelling as its type; only
|
|
1356
1376
|
// a type-qualified call is a method expression with an explicit
|
|
@@ -1392,7 +1412,7 @@ function verify(index, name, options = {}) {
|
|
|
1392
1412
|
|
|
1393
1413
|
const verifyLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
1394
1414
|
for (const call of calls) {
|
|
1395
|
-
const siteKey = `${call.file}:${call.line}`;
|
|
1415
|
+
const siteKey = `${call.file}:${call.line}:${call.calledAs || name}`;
|
|
1396
1416
|
const occurrence = verifyLineSeen.get(siteKey) || 0;
|
|
1397
1417
|
verifyLineSeen.set(siteKey, occurrence + 1);
|
|
1398
1418
|
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
@@ -2158,7 +2178,10 @@ function plan(index, name, options = {}) {
|
|
|
2158
2178
|
} else if (options.defaultValue) {
|
|
2159
2179
|
suggestion = `Add argument: ${options.defaultValue} (no default parameter values in ${planFileEntry?.language || 'this language'})`;
|
|
2160
2180
|
} else {
|
|
2161
|
-
|
|
2181
|
+
const keywordCall = langTraits(planLang)?.keywordArguments && site.keywordArgNames?.length > 0;
|
|
2182
|
+
suggestion = keywordCall
|
|
2183
|
+
? `Add keyword argument: ${options.addParam}=${options.addParam} (replace the right-hand value with the intended expression; keep existing keyword arguments)`
|
|
2184
|
+
: `Add argument: ${options.addParam}`;
|
|
2162
2185
|
}
|
|
2163
2186
|
changes.push({
|
|
2164
2187
|
file: site.file,
|
package/languages/index.js
CHANGED
|
@@ -144,6 +144,7 @@ const LANGUAGES = {
|
|
|
144
144
|
traits: {
|
|
145
145
|
...STRUCTURAL_TRAITS,
|
|
146
146
|
selfParam: ['this'],
|
|
147
|
+
storedPromises: true,
|
|
147
148
|
testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
|
|
148
149
|
testDirs: ['__tests__'],
|
|
149
150
|
},
|
|
@@ -157,6 +158,7 @@ const LANGUAGES = {
|
|
|
157
158
|
traits: {
|
|
158
159
|
...STRUCTURAL_TRAITS,
|
|
159
160
|
selfParam: ['this'],
|
|
161
|
+
storedPromises: true,
|
|
160
162
|
testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
|
|
161
163
|
testDirs: ['__tests__'],
|
|
162
164
|
},
|
|
@@ -170,6 +172,7 @@ const LANGUAGES = {
|
|
|
170
172
|
traits: {
|
|
171
173
|
...STRUCTURAL_TRAITS,
|
|
172
174
|
selfParam: ['this'],
|
|
175
|
+
storedPromises: true,
|
|
173
176
|
testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`, `${base}.test.ts`, `${base}.test.js`, `${base}.spec.ts`, `${base}.spec.js`],
|
|
174
177
|
testDirs: ['__tests__'],
|
|
175
178
|
},
|
|
@@ -324,6 +327,7 @@ const LANGUAGES = {
|
|
|
324
327
|
traits: {
|
|
325
328
|
...STRUCTURAL_TRAITS,
|
|
326
329
|
selfParam: ['this'],
|
|
330
|
+
storedPromises: true,
|
|
327
331
|
testFileCandidates: (base, ext) => [`${base}.test${ext}`, `${base}.spec${ext}`],
|
|
328
332
|
},
|
|
329
333
|
}
|
package/languages/python.js
CHANGED
|
@@ -2805,7 +2805,14 @@ function findCallsInCode(code, parser) {
|
|
|
2805
2805
|
const argsNode = node.childForFieldName('arguments');
|
|
2806
2806
|
if (argsNode) {
|
|
2807
2807
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2808
|
-
const
|
|
2808
|
+
const rawArg = argsNode.namedChild(i);
|
|
2809
|
+
const arg = rawArg.type === 'keyword_argument'
|
|
2810
|
+
? rawArg.childForFieldName('value') : rawArg;
|
|
2811
|
+
if (!arg) continue;
|
|
2812
|
+
// Bare keyword values already belong to the reference
|
|
2813
|
+
// inventory/rename path; only extend the member-value
|
|
2814
|
+
// callback model to match its positional counterpart.
|
|
2815
|
+
if (rawArg.type === 'keyword_argument' && arg.type !== 'attribute') continue;
|
|
2809
2816
|
if (arg.type === 'identifier' && !PYTHON_SKIP.has(arg.text) && !nonCallableNames.has(arg.text)) {
|
|
2810
2817
|
calls.push({
|
|
2811
2818
|
name: arg.text,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"version": "node scripts/sync-server-version.js && git add server.json",
|
|
13
|
-
"test": "node --test test/audit-5.3.7.test.js test/audit-5.3.6.test.js test/evidence-provenance.test.js test/provenance-unit.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
13
|
+
"test": "node --test test/audit-5.4.0.test.js test/audit-5.3.8.test.js test/audit-5.3.7.test.js test/audit-5.3.6.test.js test/evidence-provenance.test.js test/provenance-unit.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
14
14
|
"benchmark:agent": "node test/agent-public-surface-benchmark.js",
|
|
15
15
|
"benchmark:agent:gate": "node test/agent-public-surface-benchmark.js --gate",
|
|
16
16
|
"benchmark:agent:legacy": "node test/agent-understanding-benchmark.js",
|