ucn 5.5.2 → 5.6.0
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 +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
package/core/output/lines.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { CALLABLE_SYMBOL_KINDS } = require('../shared');
|
|
18
|
-
const { formatSurfaceMessage } = require('../registry');
|
|
18
|
+
const { CANONICAL_COMMANDS, FLAG_APPLICABILITY, formatSurfaceMessage } = require('../registry');
|
|
19
19
|
const { formatAccountLines, formatCalleeAccountLine } = require('./analysis');
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// The commands that take `lines`: the registry's applicability.
|
|
22
|
+
const LINES_COMMANDS = new Set(CANONICAL_COMMANDS.filter(command => FLAG_APPLICABILITY[command].includes('lines')));
|
|
22
23
|
|
|
23
24
|
function record(pathLike, line, text, tag = '') {
|
|
24
25
|
// One record per line is the contract: a multi-line signature or a
|
|
@@ -304,4 +305,63 @@ function formatPublicRaw(result, execution = {}) {
|
|
|
304
305
|
return code;
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
|
|
308
|
+
/**
|
|
309
|
+
* A line listing cut to `limit` characters: the records that fit, in their
|
|
310
|
+
* order, then every comment line, then one `# truncated` line counting the
|
|
311
|
+
* records left out. A record is never cut and a comment line is never
|
|
312
|
+
* dropped: the accounting still describes the whole answer. Null when the
|
|
313
|
+
* listing has no record to drop or when not one record fits beside the
|
|
314
|
+
* comment lines. `allowNoRows` may return no rows, but still keeps every
|
|
315
|
+
* comment; if that cannot fit, it explicitly withholds the whole answer.
|
|
316
|
+
*/
|
|
317
|
+
function budgetLineListing(text, limit, surface = 'cli', { allowNoRows = false } = {}) {
|
|
318
|
+
const lines = String(text).split('\n').filter(line => line !== '');
|
|
319
|
+
const records = lines.filter(line => !line.startsWith('# '));
|
|
320
|
+
const comments = lines.filter(line => line.startsWith('# '));
|
|
321
|
+
if (records.length === 0 && !allowNoRows) return null;
|
|
322
|
+
const advice = surface === 'mcp'
|
|
323
|
+
? 'raise max_chars or narrow the query'
|
|
324
|
+
: 'raise --max-chars or omit it';
|
|
325
|
+
const trailer = omitted => `# truncated: ${omitted} of ${records.length} rows omitted; ${advice}`;
|
|
326
|
+
// The most records whose text, with every comment line and the trailer
|
|
327
|
+
// that counts the rest, is within the limit.
|
|
328
|
+
const fixed = comments.reduce((sum, line) => sum + line.length + 1, 0);
|
|
329
|
+
const withheld = () => {
|
|
330
|
+
const flag = surface === 'mcp' ? 'max_chars' : '--max-chars';
|
|
331
|
+
const needed = Math.min(String(text).length, fixed + trailer(records.length).length);
|
|
332
|
+
const notices = [
|
|
333
|
+
`# Answer withheld: accounting does not fit; raise ${flag} to at least ${needed}.`,
|
|
334
|
+
`# Answer withheld; ${flag}>=${needed}`,
|
|
335
|
+
`# withheld; >=${needed}`,
|
|
336
|
+
];
|
|
337
|
+
return {
|
|
338
|
+
text: notices.find(notice => notice.length <= limit) || '',
|
|
339
|
+
rows: records.length, omitted: records.length, contractMetadataComplete: false,
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
if (records.length === 0) return withheld();
|
|
343
|
+
const prefix = [0];
|
|
344
|
+
for (const record of records) prefix.push(prefix[prefix.length - 1] + record.length + 1);
|
|
345
|
+
let kept = 0;
|
|
346
|
+
for (let count = records.length - 1; count >= 1; count--) {
|
|
347
|
+
if (prefix[count] + fixed + trailer(records.length - count).length <= limit) { kept = count; break; }
|
|
348
|
+
}
|
|
349
|
+
if (kept === 0) {
|
|
350
|
+
// No row fits. Never present a partial accounting as the complete
|
|
351
|
+
// answer: all comment lines accompany the row count, or the entire
|
|
352
|
+
// answer is withheld explicitly.
|
|
353
|
+
const all = trailer(records.length);
|
|
354
|
+
if (!allowNoRows) return null;
|
|
355
|
+
if (fixed + all.length > limit) return withheld();
|
|
356
|
+
return { text: [...comments, all].join('\n'), rows: records.length, omitted: records.length,
|
|
357
|
+
contractMetadataComplete: true };
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
text: [...records.slice(0, kept), ...comments, trailer(records.length - kept)].join('\n'),
|
|
361
|
+
rows: records.length,
|
|
362
|
+
omitted: records.length - kept,
|
|
363
|
+
contractMetadataComplete: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = { LINES_COMMANDS, formatPublicLines, formatPublicRaw, commentLines, budgetLineListing };
|
package/core/output/public.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
const { COMMAND_CONTRACTS } = require('../command-contracts');
|
|
12
12
|
const { COMMAND_TRUST_MATRIX } = require('../trust-matrix');
|
|
13
|
-
const { toCliName, toMcpName, formatSurfaceMessage } = require('../registry');
|
|
13
|
+
const { toCliName, toMcpName, formatSurfaceMessage, paramDefault } = require('../registry');
|
|
14
14
|
const { LINES_COMMANDS, formatPublicLines, formatPublicRaw } = require('./lines');
|
|
15
15
|
const { provenanceReplacer } = require('./provenance');
|
|
16
16
|
|
|
17
|
+
const { summarizeReasons } = require('../evidence-bands');
|
|
18
|
+
|
|
17
19
|
const legacy = {
|
|
18
20
|
...require('./analysis'),
|
|
19
21
|
...require('./analysis-ext'),
|
|
@@ -163,11 +165,12 @@ function formatShow(result, params = {}, hints = presentationHints()) {
|
|
|
163
165
|
if (result.context) {
|
|
164
166
|
const context = { ...result.context };
|
|
165
167
|
const formatted = legacy.formatContext(context, {
|
|
166
|
-
showConfidence: params.showConfidence
|
|
167
|
-
compact: params.compact
|
|
168
|
+
showConfidence: params.showConfidence ?? paramDefault('showConfidence'),
|
|
169
|
+
compact: params.compact ?? paramDefault('compact', 'show'),
|
|
168
170
|
expandHint: hints.source,
|
|
169
171
|
allHint: hints.all,
|
|
170
172
|
usagesHint: hints.usages(context.function || result.target),
|
|
173
|
+
evidenceBands: hints.evidenceBands,
|
|
171
174
|
});
|
|
172
175
|
parts.push(block('RELATIONSHIPS', projectContextText(formatted.text, selected)));
|
|
173
176
|
}
|
|
@@ -227,10 +230,14 @@ function formatTrace(result, params = {}, hints = presentationHints()) {
|
|
|
227
230
|
});
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
function formatImpact(result, params = {}) {
|
|
233
|
+
function formatImpact(result, params = {}, hints = presentationHints()) {
|
|
234
|
+
// `--all` prints every unverified row, `--top` (or `--limit`) that many.
|
|
235
|
+
const top = Number(params.limit) > 0 ? Number(params.limit) : Number(params.top) > 0 ? Number(params.top) : undefined;
|
|
236
|
+
const rows = { all: !!params.all, ...(Number.isInteger(top) && { top }), allHint: hints.all,
|
|
237
|
+
evidenceBands: hints.evidenceBands };
|
|
231
238
|
return modeOf('impact', result) === 'diff'
|
|
232
|
-
? legacy.formatDiffImpact(result,
|
|
233
|
-
: legacy.formatImpact(result, { compact: params.compact
|
|
239
|
+
? legacy.formatDiffImpact(result, rows)
|
|
240
|
+
: legacy.formatImpact(result, { compact: params.compact ?? paramDefault('compact', 'impact'), ...rows });
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
function formatTests(result, params = {}, hints = presentationHints()) {
|
|
@@ -299,11 +306,13 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
299
306
|
if (shaped != null) return shaped;
|
|
300
307
|
}
|
|
301
308
|
const hints = presentationHints(execution.surface);
|
|
309
|
+
// Display only: another band table relabels the reason summaries.
|
|
310
|
+
if (execution.evidenceBands) hints.evidenceBands = execution.evidenceBands;
|
|
302
311
|
if (result?.scopeWarning?.hint) {
|
|
303
|
-
result
|
|
312
|
+
result = { ...result, scopeWarning: {
|
|
304
313
|
...result.scopeWarning,
|
|
305
314
|
hint: formatSurfaceMessage(result.scopeWarning.hint, execution.surface),
|
|
306
|
-
};
|
|
315
|
+
} };
|
|
307
316
|
}
|
|
308
317
|
let text;
|
|
309
318
|
switch (command) {
|
|
@@ -312,7 +321,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
312
321
|
text = modeOf('find', result) === 'type'
|
|
313
322
|
? legacy.formatTypedef(result, params.name)
|
|
314
323
|
: legacy.formatFindDetailed(result, params.name, {
|
|
315
|
-
compact: params.compact,
|
|
324
|
+
compact: params.compact ?? paramDefault('compact', 'find'),
|
|
316
325
|
withSource: params.withSource,
|
|
317
326
|
top: params.limit,
|
|
318
327
|
all: !!params.all,
|
|
@@ -320,7 +329,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
320
329
|
});
|
|
321
330
|
break;
|
|
322
331
|
case 'usages': text = legacy.formatUsages(result, params.name, {
|
|
323
|
-
compact: params.compact,
|
|
332
|
+
compact: params.compact ?? paramDefault('compact', 'usages'),
|
|
324
333
|
all: params.all,
|
|
325
334
|
allHint: hints.all,
|
|
326
335
|
}); break;
|
|
@@ -341,7 +350,7 @@ function formatPublicText(command, result, params = {}, execution = {}) {
|
|
|
341
350
|
break;
|
|
342
351
|
case 'source': text = formatSource(result, hints); break;
|
|
343
352
|
case 'trace': text = formatTrace(result, params, hints); break;
|
|
344
|
-
case 'impact': text = formatImpact(result, params); break;
|
|
353
|
+
case 'impact': text = formatImpact(result, params, hints); break;
|
|
345
354
|
case 'tests': text = formatTests(result, params, hints); break;
|
|
346
355
|
case 'deps': text = formatDeps(result, params, hints); break;
|
|
347
356
|
case 'api': text = legacy.formatApi(result, params.file || '.'); break;
|
|
@@ -413,6 +422,8 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
413
422
|
commandMeta.excludedConstructors = result.excludedConstructors || 0;
|
|
414
423
|
commandMeta.pythonImplicitExportFiles = result.pythonImplicitExportFiles || 0;
|
|
415
424
|
commandMeta.excludedDynamicDispatch = result.excludedDynamicDispatch || 0;
|
|
425
|
+
commandMeta.excludedRequiredSlot = result.excludedRequiredSlot || 0;
|
|
426
|
+
commandMeta.projectKind = result.projectKind || 'library';
|
|
416
427
|
commandMeta.computedDispatch = result.computedDispatch || { count: 0, names: [] };
|
|
417
428
|
commandMeta.reflection = result.reflection || {
|
|
418
429
|
count: 0, literalCount: 0, dynamicCount: 0,
|
|
@@ -468,6 +479,13 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
468
479
|
if (command === 'usages' && result?.analysisGaps) {
|
|
469
480
|
commandMeta.analysisGaps = result.analysisGaps;
|
|
470
481
|
}
|
|
482
|
+
// The reasons of the unverified callers with how often each was a real
|
|
483
|
+
// edge on the release board: labels beside the data, never part of it.
|
|
484
|
+
const unverifiedCallers = command === 'impact' ? result?.unverifiedSites
|
|
485
|
+
: command === 'show' ? result?.context?.unverifiedCallers : null;
|
|
486
|
+
if (Array.isArray(unverifiedCallers) && unverifiedCallers.length > 0) {
|
|
487
|
+
commandMeta.unverifiedReasons = summarizeReasons(unverifiedCallers, execution.evidenceBands);
|
|
488
|
+
}
|
|
471
489
|
|
|
472
490
|
const surfaceCommand = execution.surface === 'mcp'
|
|
473
491
|
? toMcpName(command)
|
|
@@ -476,6 +494,7 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
|
|
|
476
494
|
const envelope = {
|
|
477
495
|
meta: {
|
|
478
496
|
command: surfaceCommand,
|
|
497
|
+
...(execution.surface === 'cli' && { maxChars: 'not-applied' }),
|
|
479
498
|
...(surfaceCommand !== command && { canonicalCommand: command }),
|
|
480
499
|
// A result that self-reports it could not run (check with a bad
|
|
481
500
|
// base ref / outside git) must be machine-distinguishable from a
|
|
@@ -71,10 +71,42 @@ function groupedReviewLines(items, cap = 20) {
|
|
|
71
71
|
* Format plan command output - text
|
|
72
72
|
* Shows before/after signatures and all changes needed
|
|
73
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* A deletion assessment: the verdict, its basis and where the declaration
|
|
76
|
+
* is. What the verdict never means (a guarantee) is stated once, in the
|
|
77
|
+
* tool description and the usage guide.
|
|
78
|
+
*/
|
|
79
|
+
function formatDeletion(assessment) {
|
|
80
|
+
const d = assessment.declaration;
|
|
81
|
+
const owner = d.className ? `${d.className}.` : '';
|
|
82
|
+
const lines = [
|
|
83
|
+
`Deletion assessment: ${owner}${d.name}`,
|
|
84
|
+
'═'.repeat(60),
|
|
85
|
+
`${d.file}:${d.startLine}${d.type ? ` (${d.type})` : ''}`,
|
|
86
|
+
...(assessment.warnings || []).map(warning => `Note: ${warning.message}`),
|
|
87
|
+
`VERDICT: ${assessment.verdict}`,
|
|
88
|
+
` basis: ${assessment.basis}`,
|
|
89
|
+
];
|
|
90
|
+
if (assessment.site) {
|
|
91
|
+
const site = assessment.site;
|
|
92
|
+
lines.push(` first use: ${site.file}${site.line != null ? `:${site.line}` : ''}`);
|
|
93
|
+
}
|
|
94
|
+
if (assessment.projectKind === 'application') {
|
|
95
|
+
lines.push(' project kind: application (.ucn.json): exported symbols are assessed');
|
|
96
|
+
}
|
|
97
|
+
if (assessment.verdict === 'has-static-dependents') {
|
|
98
|
+
lines.push(` next: ucn show ${d.file}:${d.startLine}:${d.name} lists the callers; ucn usages ${d.name} lists every occurrence`);
|
|
99
|
+
} else if (assessment.reason === 'downstream-exposure') {
|
|
100
|
+
lines.push(' next: declare "kind": "application" in .ucn.json if nothing outside this repository consumes it');
|
|
101
|
+
}
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
function formatPlan(plan, options = {}) {
|
|
75
106
|
if (!plan) {
|
|
76
107
|
return 'Function not found.';
|
|
77
108
|
}
|
|
109
|
+
if (plan.operation === 'delete') return formatDeletion(plan);
|
|
78
110
|
if (!plan.found) {
|
|
79
111
|
return `Function "${plan.function}" not found.`;
|
|
80
112
|
}
|
package/core/output/reporting.js
CHANGED
|
@@ -215,7 +215,8 @@ function formatDeadcode(results, options = {}) {
|
|
|
215
215
|
if (results.length === 0 && !results.excludedDecorated &&
|
|
216
216
|
!results.excludedExported && !results.excludedExternalContract &&
|
|
217
217
|
!results.excludedRuntimeContract && !results.excludedConstructors && !results.pythonImplicitExportFiles &&
|
|
218
|
-
!results.excludedDynamicDispatch && !results.
|
|
218
|
+
!results.excludedDynamicDispatch && !results.excludedRequiredSlot &&
|
|
219
|
+
results.projectKind !== 'application' && !results.computedDispatch?.count &&
|
|
219
220
|
!results.reflection?.withheld && !results.reflection?.dynamicCount && !results.reflection?.literalCount &&
|
|
220
221
|
results.coverage?.complete !== false) {
|
|
221
222
|
return 'No dead code found.';
|
|
@@ -301,6 +302,12 @@ function formatDeadcode(results, options = {}) {
|
|
|
301
302
|
if (results.excludedDynamicDispatch > 0) {
|
|
302
303
|
lines.push(`\n${results.excludedDynamicDispatch} registry member(s) hidden because a matching computed dispatch (registry[key]()) may invoke them.`);
|
|
303
304
|
}
|
|
305
|
+
if (results.excludedRequiredSlot > 0) {
|
|
306
|
+
lines.push(`\n${results.excludedRequiredSlot} member(s) withheld: required by an interface, abstract class or trait of the project, overridden by a descendant that says so, or matching a project interface or protocol by shape. Removing one can break a type.`);
|
|
307
|
+
}
|
|
308
|
+
if (results.projectKind === 'application') {
|
|
309
|
+
lines.push('\nProject kind: application (.ucn.json). Exported symbols are audited; contracts, registrations and reflection still withhold.');
|
|
310
|
+
}
|
|
304
311
|
if (results.computedDispatch?.count > 0) {
|
|
305
312
|
lines.push(`\nWARNING: ${results.computedDispatch.count} computed dispatch call(s) in ${results.computedDispatch.fileCount} file(s). Dead-code results are review candidates; runtime-selected members may not have a named static edge.`);
|
|
306
313
|
}
|
|
@@ -315,6 +322,12 @@ function formatDeadcode(results, options = {}) {
|
|
|
315
322
|
if (results.reflection?.dynamicCount > 0) {
|
|
316
323
|
lines.push(`\nWARNING: ${results.reflection.dynamicCount} dynamic reflection use(s) have no static member spelling. Dead-code results remain review candidates because runtime-selected members cannot be attributed.`);
|
|
317
324
|
}
|
|
325
|
+
if (results.reflection?.withheldByTable?.length > 0) {
|
|
326
|
+
lines.push('\nConservatively withheld: exact string literals in a file with dynamic reflection (flow to the reflection call is unproven):');
|
|
327
|
+
for (const site of results.reflection.withheldByTable) {
|
|
328
|
+
lines.push(` ${site.name}: named in possible reflection table ${site.file}:${site.line}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
318
331
|
if (results.macroPaste) {
|
|
319
332
|
const m = results.macroPaste;
|
|
320
333
|
if (m.withheld > 0) {
|
|
@@ -373,6 +386,8 @@ function formatDeadcodeJson(results) {
|
|
|
373
386
|
...(results.excludedConstructors > 0 && { excludedConstructors: results.excludedConstructors }),
|
|
374
387
|
...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
|
|
375
388
|
...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
|
|
389
|
+
...(results.excludedRequiredSlot > 0 && { excludedRequiredSlot: results.excludedRequiredSlot }),
|
|
390
|
+
...(results.projectKind === 'application' && { projectKind: results.projectKind }),
|
|
376
391
|
...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
|
|
377
392
|
...(results.reflection?.count > 0 && { reflection: results.reflection }),
|
|
378
393
|
...(results.macroPaste && { macroPaste: results.macroPaste }),
|
|
@@ -567,7 +582,11 @@ function formatOrient(result, options = {}) {
|
|
|
567
582
|
if (result.skippedSources?.count > 0) {
|
|
568
583
|
const reasons = Object.entries(result.skippedSources.reasons || {})
|
|
569
584
|
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
570
|
-
|
|
585
|
+
const unsearchable = result.skippedSources.unsearchable || 0;
|
|
586
|
+
lines.push(`SKIPPED SOURCE: ${result.skippedSources.count} path(s) not indexed${reasons ? ` (${reasons})` : ''} — ` +
|
|
587
|
+
(unsearchable > 0
|
|
588
|
+
? `${unsearchable} could not be searched, so no caller account is complete.`
|
|
589
|
+
: 'searched as text for each name; an occurrence there is listed, not analyzed.'));
|
|
571
590
|
}
|
|
572
591
|
lines.push('');
|
|
573
592
|
|
package/core/output/search.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { detectDoubleEscaping, advisoryLine } = require('./shared');
|
|
6
|
+
const { selectorText, testCountText } = require('../test-units');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Format search command output
|
|
@@ -419,16 +420,24 @@ function formatTests(tests, name) {
|
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
for (const testFile of tests) {
|
|
422
|
-
|
|
423
|
+
const counted = testCountText(testFile.tests, testFile.moreTests, testFile.conditionalTests);
|
|
424
|
+
const facts = testFile.role === 'support' ? ' [support: declares no test]'
|
|
425
|
+
: counted ? ` (${counted})` : '';
|
|
426
|
+
lines.push(`${testFile.file}${facts}`);
|
|
423
427
|
for (const match of testFile.matches) {
|
|
424
428
|
const typeLabel = match.matchType === 'test-case' ? '[test]' :
|
|
425
429
|
match.matchType === 'import' ? '[import]' :
|
|
426
430
|
match.matchType === 'call' ? '[call]' :
|
|
427
431
|
match.matchType === 'unverified-call' ? '[unverified-call]' :
|
|
428
432
|
match.matchType === 'unverified-reference' ? '[unverified-ref]' :
|
|
433
|
+
match.matchType === 'inherited-test' ? '[inherited]' :
|
|
429
434
|
match.matchType === 'string-ref' ? '[string]' : '[ref]';
|
|
430
|
-
|
|
435
|
+
// A test the class inherits: its name, and where its body is.
|
|
436
|
+
const origin = match.matchType === 'inherited-test' ? ` ${match.name} (from ${match.from})` : '';
|
|
437
|
+
lines.push(` ${match.line}: ${typeLabel} ${match.content}${origin}`);
|
|
431
438
|
}
|
|
439
|
+
if (testFile.run) lines.push(` run: ${selectorText(testFile.run)}`);
|
|
440
|
+
else if (testFile.role === 'tests' && testFile.runWithheld) lines.push(` no command: ${testFile.runWithheld}`);
|
|
432
441
|
lines.push('');
|
|
433
442
|
}
|
|
434
443
|
}
|
package/core/output/tracing.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* conservation) lines close the arithmetic.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const { testCountText } = require('../test-units');
|
|
11
12
|
const { unverifiedReasonLabel } = require('./shared');
|
|
12
13
|
const { codeUnitCompare } = require('../shared');
|
|
13
14
|
const { formatAccountLines } = require('./analysis');
|
|
@@ -562,23 +563,49 @@ function formatAffectedTests(result, options = {}) {
|
|
|
562
563
|
lines.push('No confirmed static test links found for any affected function.');
|
|
563
564
|
} else {
|
|
564
565
|
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
|
|
566
|
+
const runnable = result.testFiles.filter(tf => tf.role !== 'support');
|
|
567
|
+
const support = result.testFiles.filter(tf => tf.role === 'support');
|
|
568
|
+
const displayFiles = runnable.slice(0, MAX_TEST_FILES);
|
|
569
|
+
const truncatedFiles = runnable.length - displayFiles.length;
|
|
570
|
+
// A count is exact only where every test of the files is listed:
|
|
571
|
+
// "at least" says a file holds tests that are not.
|
|
572
|
+
const total = testCountText(summary.totalTests, summary.moreTests, summary.conditionalTests);
|
|
573
|
+
lines.push(`Test files to run (${runnable.length})${total ? `, ${total}` : ''}:`);
|
|
568
574
|
lines.push('');
|
|
569
575
|
for (const tf of displayFiles) {
|
|
570
|
-
|
|
576
|
+
const counted = testCountText(tf.testCount, tf.moreTests, tf.conditionalTests);
|
|
577
|
+
const count = counted ? ` · ${counted}` : '';
|
|
578
|
+
const refusal = tf.runWithheld ? ` · no command: ${tf.runWithheld}` : '';
|
|
579
|
+
lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})${count}${refusal}`);
|
|
571
580
|
// Show up to 5 key matches per file
|
|
572
581
|
const keyMatches = tf.matches
|
|
573
|
-
.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
582
|
+
.filter(m => m.matchType === 'call' || m.matchType === 'test-case' || m.matchType === 'inherited-test')
|
|
574
583
|
.slice(0, 5);
|
|
575
584
|
for (const m of keyMatches) {
|
|
576
|
-
|
|
585
|
+
const origin = m.matchType === 'inherited-test' ? ` ${m.name} (from ${m.from})` : '';
|
|
586
|
+
lines.push(` L${m.line}: ${m.content}${origin} [${m.matchType}]`);
|
|
577
587
|
}
|
|
578
588
|
}
|
|
579
589
|
if (truncatedFiles > 0) {
|
|
580
590
|
lines.push(`\n ... ${truncatedFiles} more test files (use file= and exclude= to narrow scope)`);
|
|
581
591
|
}
|
|
592
|
+
if (support.length > 0) {
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push(`Test support code reached (${support.length}) — declares no test, runs with the tests that load it:`);
|
|
595
|
+
for (const tf of support.slice(0, options.all ? Infinity : 10)) {
|
|
596
|
+
lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const commands = result.commands || [];
|
|
600
|
+
if (commands.length > 0) {
|
|
601
|
+
lines.push('');
|
|
602
|
+
lines.push('Run:');
|
|
603
|
+
const MAX_COMMANDS = options.all ? Infinity : 5;
|
|
604
|
+
for (const line of commands.slice(0, MAX_COMMANDS)) lines.push(` ${line}`);
|
|
605
|
+
if (commands.length > MAX_COMMANDS) {
|
|
606
|
+
lines.push(` ... ${commands.length - MAX_COMMANDS} more (${options.allHint || 'use --all'})`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
582
609
|
}
|
|
583
610
|
|
|
584
611
|
// Possible band: functions reachable only through unverified chains.
|
|
@@ -614,7 +641,8 @@ function formatAffectedTests(result, options = {}) {
|
|
|
614
641
|
const pct = summary.totalAffected > 0
|
|
615
642
|
? Math.round(summary.staticallyLinkedFunctions / summary.totalAffected * 100)
|
|
616
643
|
: 0;
|
|
617
|
-
|
|
644
|
+
const totalTests = testCountText(summary.totalTests, summary.moreTests, summary.conditionalTests);
|
|
645
|
+
let summaryLine = `Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} statically linked test files${totalTests ? ` (${totalTests})` : ''}, ${summary.staticallyLinkedFunctions}/${summary.totalAffected} functions linked (${pct}%)`;
|
|
618
646
|
if (summary.possiblyAffected > 0) {
|
|
619
647
|
summaryLine += ` · ${summary.possiblyAffected} possibly affected (unverified chains)`;
|
|
620
648
|
}
|