ucn 4.2.3 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/output/reporting.js
CHANGED
|
@@ -9,6 +9,15 @@ const {
|
|
|
9
9
|
formatClassSignature,
|
|
10
10
|
} = require('./shared');
|
|
11
11
|
|
|
12
|
+
const LIST_TEXT_LIMIT = 240;
|
|
13
|
+
|
|
14
|
+
function elideListText(value, limit = LIST_TEXT_LIMIT) {
|
|
15
|
+
const text = String(value ?? '');
|
|
16
|
+
if (text.length <= limit) return text;
|
|
17
|
+
const kept = limit - 18;
|
|
18
|
+
return `${text.slice(0, kept)}… [${text.length} chars]`;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Format toc command output
|
|
14
23
|
* @param {object} toc - TOC data
|
|
@@ -63,10 +72,10 @@ function formatToc(toc, options = {}) {
|
|
|
63
72
|
lines.push(`\n${file.file} (${parts.join(', ')})`);
|
|
64
73
|
if (file.symbols) {
|
|
65
74
|
for (const fn of file.symbols.functions) {
|
|
66
|
-
lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${formatFunctionSignature(fn)}`);
|
|
75
|
+
lines.push(` ${lineRange(fn.startLine, fn.endLine)} ${elideListText(formatFunctionSignature(fn))}`);
|
|
67
76
|
}
|
|
68
77
|
for (const cls of file.symbols.classes) {
|
|
69
|
-
lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${formatClassSignature(cls)}`);
|
|
78
|
+
lines.push(` ${lineRange(cls.startLine, cls.endLine)} ${elideListText(formatClassSignature(cls))}`);
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
} else {
|
|
@@ -143,23 +152,26 @@ function formatStats(stats, options = {}) {
|
|
|
143
152
|
lines.push(`\nFunctions by line count (top ${shown.length} of ${stats.functions.length}):`);
|
|
144
153
|
for (const fn of shown) {
|
|
145
154
|
const loc = `${fn.file}:${fn.startLine}`;
|
|
146
|
-
lines.push(` ${String(fn.lines).padStart(5)} lines ${fn.name} (${loc})`);
|
|
155
|
+
lines.push(` ${String(fn.lines).padStart(5)} lines ${elideListText(fn.name, 100)} (${loc})`);
|
|
147
156
|
}
|
|
148
157
|
if (stats.functions.length > top) {
|
|
149
|
-
lines.push(` ... ${stats.functions.length - top} more (use --top=N to show more)`);
|
|
158
|
+
lines.push(` ... ${stats.functions.length - top} more (${options.topHint || 'use --top=N to show more'})`);
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
if (stats.hot) {
|
|
154
163
|
const items = stats.hot.items || [];
|
|
155
164
|
const total = stats.hot.total || items.length;
|
|
156
|
-
|
|
165
|
+
const population = stats.hot.totalKind === 'raw-call-candidates'
|
|
166
|
+
? `${total} raw candidates`
|
|
167
|
+
: `${total} called`;
|
|
168
|
+
lines.push(`\nHottest functions (top ${items.length} of ${population}):`);
|
|
157
169
|
if (items.length === 0) {
|
|
158
170
|
lines.push(' (no inbound calls detected)');
|
|
159
171
|
} else {
|
|
160
172
|
for (const fn of items) {
|
|
161
173
|
const loc = `${fn.file}:${fn.startLine}`;
|
|
162
|
-
lines.push(` ${String(fn.callCount).padStart(5)} calls ${fn.name} (${loc})`);
|
|
174
|
+
lines.push(` ${String(fn.callCount).padStart(5)} calls ${elideListText(fn.name, 100)} (${loc})`);
|
|
163
175
|
// MEDIUM-6: when the same name has multiple definitions across
|
|
164
176
|
// files (e.g. test helpers vs. test fixtures both named `tmp`),
|
|
165
177
|
// list the additional locations indented so the user knows
|
|
@@ -172,7 +184,7 @@ function formatStats(stats, options = {}) {
|
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
if (total > items.length) {
|
|
175
|
-
lines.push(` ... ${total - items.length} more (use --top=N to show more)`);
|
|
187
|
+
lines.push(` ... ${total - items.length} more (${options.topHint || 'use --top=N to show more'})`);
|
|
176
188
|
}
|
|
177
189
|
}
|
|
178
190
|
}
|
|
@@ -194,7 +206,11 @@ function formatStatsJson(stats) {
|
|
|
194
206
|
* @param {string} [options.exportedHint] - Hint about exported symbols exclusion
|
|
195
207
|
*/
|
|
196
208
|
function formatDeadcode(results, options = {}) {
|
|
197
|
-
if (results.length === 0 && !results.excludedDecorated &&
|
|
209
|
+
if (results.length === 0 && !results.excludedDecorated &&
|
|
210
|
+
!results.excludedExported && !results.excludedExternalContract &&
|
|
211
|
+
!results.excludedRuntimeContract && !results.pythonImplicitExportFiles &&
|
|
212
|
+
!results.excludedDynamicDispatch && !results.computedDispatch?.count &&
|
|
213
|
+
results.coverage?.complete !== false) {
|
|
198
214
|
return 'No dead code found.';
|
|
199
215
|
}
|
|
200
216
|
|
|
@@ -238,12 +254,16 @@ function formatDeadcode(results, options = {}) {
|
|
|
238
254
|
: '';
|
|
239
255
|
// The only references are the symbol's own recursion (fix #253c).
|
|
240
256
|
const recStr = item.selfRecursive ? ' [only self-references — recursive]' : '';
|
|
241
|
-
const displayName =
|
|
257
|
+
const displayName = elideListText(
|
|
258
|
+
item.className ? `${item.className}.${item.name}` : item.name,
|
|
259
|
+
120,
|
|
260
|
+
);
|
|
242
261
|
lines.push(` ${lineRange(item.startLine, item.endLine)} ${displayName} (${item.type})${exported}${hintStr}${declStr}${extStr}${recStr}`);
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
if (hidden > 0) {
|
|
246
|
-
lines.push(`\n${hidden} more result(s) not shown.
|
|
265
|
+
lines.push(`\n${hidden} more result(s) not shown. ${options.topHint ||
|
|
266
|
+
`Use --top=${results.length} or --all to see all.`}`);
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
// Show counts of excluded items with expansion hints
|
|
@@ -262,6 +282,29 @@ function formatDeadcode(results, options = {}) {
|
|
|
262
282
|
const extHint = options.externalContractHint || `${results.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.`;
|
|
263
283
|
lines.push(`\n${extHint}`);
|
|
264
284
|
}
|
|
285
|
+
if (results.excludedRuntimeContract > 0) {
|
|
286
|
+
lines.push(`\n${results.excludedRuntimeContract} Java serialization callback(s) hidden (JVM runtime contract, not dead).`);
|
|
287
|
+
}
|
|
288
|
+
if (results.pythonImplicitExportFiles > 0) {
|
|
289
|
+
lines.push(`\nPython public-surface rule active in ${results.pythonImplicitExportFiles} file(s) without __all__: top-level non-underscore names are treated as externally reachable.`);
|
|
290
|
+
}
|
|
291
|
+
if (results.excludedDynamicDispatch > 0) {
|
|
292
|
+
lines.push(`\n${results.excludedDynamicDispatch} registry member(s) hidden because a matching computed dispatch (registry[key]()) may invoke them.`);
|
|
293
|
+
}
|
|
294
|
+
if (results.computedDispatch?.count > 0) {
|
|
295
|
+
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.`);
|
|
296
|
+
}
|
|
297
|
+
if (results.coverage?.complete === false) {
|
|
298
|
+
const c = results.coverage;
|
|
299
|
+
const reasonText = Object.entries(c.reasons || {})
|
|
300
|
+
.map(([reason, count]) => `${count} ${reason}`)
|
|
301
|
+
.join(', ');
|
|
302
|
+
if (c.claimsWithdrawn) {
|
|
303
|
+
lines.push(`\nWARNING: dead-code claims withdrawn because source coverage is incomplete (${reasonText || 'unknown gap'}).`);
|
|
304
|
+
} else {
|
|
305
|
+
lines.push(`\nWARNING: source coverage is incomplete (${reasonText || 'unknown gap'}); ${c.suppressedMatched || 0} candidate name(s) found in skipped source were suppressed.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
265
308
|
|
|
266
309
|
if (lines.length === 0) {
|
|
267
310
|
return 'No dead code found.';
|
|
@@ -290,6 +333,11 @@ function formatDeadcodeJson(results) {
|
|
|
290
333
|
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
291
334
|
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
292
335
|
...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
|
|
336
|
+
...(results.excludedRuntimeContract > 0 && { excludedRuntimeContract: results.excludedRuntimeContract }),
|
|
337
|
+
...(results.pythonImplicitExportFiles > 0 && { pythonImplicitExportFiles: results.pythonImplicitExportFiles }),
|
|
338
|
+
...(results.excludedDynamicDispatch > 0 && { excludedDynamicDispatch: results.excludedDynamicDispatch }),
|
|
339
|
+
...(results.computedDispatch?.count > 0 && { computedDispatch: results.computedDispatch }),
|
|
340
|
+
...(results.coverage?.complete === false && { coverage: results.coverage }),
|
|
293
341
|
symbols: results.map(item => {
|
|
294
342
|
const handleSym = { ...item, relativePath: item.relativePath || item.file };
|
|
295
343
|
const handle = formatSymbolHandle(handleSym);
|
|
@@ -400,9 +448,9 @@ function formatEntrypointsJson(results) {
|
|
|
400
448
|
/**
|
|
401
449
|
* formatOrient — one-screen cold-repo orientation.
|
|
402
450
|
*/
|
|
403
|
-
function formatOrient(result) {
|
|
451
|
+
function formatOrient(result, options = {}) {
|
|
404
452
|
const lines = [];
|
|
405
|
-
lines.push(`PROJECT ORIENTATION — ${result.root}`);
|
|
453
|
+
lines.push(`PROJECT ORIENTATION — ${result.root}${result.scope ? ` (scoped to ${result.scope})` : ''}`);
|
|
406
454
|
lines.push('═'.repeat(60));
|
|
407
455
|
|
|
408
456
|
// Size + language mix (percent by symbols, largest first)
|
|
@@ -413,7 +461,8 @@ function formatOrient(result) {
|
|
|
413
461
|
const langStr = langs
|
|
414
462
|
.map(l => `${l.lang} ${Math.round((l.symbols / totalSym) * 100)}%`)
|
|
415
463
|
.join(', ');
|
|
416
|
-
lines.push(`${result.files} files · ${result.symbols} symbols ·
|
|
464
|
+
lines.push(`${result.files} files · ${result.symbols} symbols · ` +
|
|
465
|
+
`language mix by symbols: ${langStr || 'none'}`);
|
|
417
466
|
lines.push('');
|
|
418
467
|
|
|
419
468
|
if (result.dirs?.length) {
|
|
@@ -427,7 +476,10 @@ function formatOrient(result) {
|
|
|
427
476
|
|
|
428
477
|
if (result.hot?.items?.length) {
|
|
429
478
|
const scope = result.hot.production ? 'production functions' : 'functions';
|
|
430
|
-
|
|
479
|
+
const population = result.hot.totalKind === 'raw-call-candidates'
|
|
480
|
+
? `${result.hot.total} raw candidates`
|
|
481
|
+
: result.hot.total;
|
|
482
|
+
lines.push(`HOT (most-called ${scope}, top ${result.hot.items.length} of ${population}):`);
|
|
431
483
|
for (const h of result.hot.items) {
|
|
432
484
|
const label = h.className ? `${h.className}.${h.name}` : h.name;
|
|
433
485
|
lines.push(` ${label} — ${h.callCount} call(s) · ${h.file}:${h.line}`);
|
|
@@ -438,23 +490,48 @@ function formatOrient(result) {
|
|
|
438
490
|
if (result.entrypoints) {
|
|
439
491
|
const byType = result.entrypoints.byType
|
|
440
492
|
.map(t => `${t.type} ${t.count}`).join(', ');
|
|
441
|
-
lines.push(`ENTRY POINTS: ${result.entrypoints.total} — ${byType}`);
|
|
493
|
+
lines.push(`ENTRY POINTS: ${result.entrypoints.total}${byType ? ` — ${byType}` : ''}`);
|
|
442
494
|
} else {
|
|
443
495
|
lines.push('ENTRY POINTS: (detection unavailable)');
|
|
444
496
|
}
|
|
445
497
|
|
|
446
498
|
const bs = result.trust?.blindSpots || {};
|
|
447
499
|
const bsParts = [];
|
|
448
|
-
if (bs.dynamicImports)
|
|
500
|
+
if (bs.dynamicImports) {
|
|
501
|
+
const note = dynamicImportsNote(bs.dynamicImports, {
|
|
502
|
+
projectLanguage: result.projectLanguage || result.trust?.projectLanguage,
|
|
503
|
+
});
|
|
504
|
+
if (note) bsParts.push(note);
|
|
505
|
+
}
|
|
449
506
|
if (bs.evalCalls) bsParts.push(`${bs.evalCalls} eval`);
|
|
450
507
|
if (bs.reflection) bsParts.push(`${bs.reflection} reflection`);
|
|
508
|
+
if (bs.computedDispatch) bsParts.push(`${bs.computedDispatch} computed dispatch`);
|
|
451
509
|
if (bs.parseFailures) bsParts.push(`${bs.parseFailures} parse failure(s)`);
|
|
452
|
-
|
|
510
|
+
if (bs.parseRecoveries) bsParts.push(`${bs.parseRecoveries} parser-recovery file(s)`);
|
|
511
|
+
if (bs.unsupportedSources) bsParts.push(`${bs.unsupportedSources} unsupported source file(s)`);
|
|
512
|
+
if (bs.skippedSources) bsParts.push(`${bs.skippedSources} skipped source path(s)`);
|
|
513
|
+
lines.push(`TRUST: ${result.trust?.level || 'UNKNOWN'}${bsParts.length ? ' — ' + bsParts.join(', ') : ''} (${options.healthHint || 'ucn repo --sections=health --deep for detail'})`);
|
|
514
|
+
if (result.unsupportedSources?.count > 0) {
|
|
515
|
+
const languages = Object.entries(result.unsupportedSources.languages || {})
|
|
516
|
+
.map(([language, count]) => `${language} ${count}`)
|
|
517
|
+
.join(', ');
|
|
518
|
+
lines.push(`SKIPPED SOURCE: ${result.unsupportedSources.count} file(s)${languages ? ` (${languages})` : ''} — use grep/ripgrep plus a language-native analyzer.`);
|
|
519
|
+
}
|
|
520
|
+
if (result.skippedSources?.count > 0) {
|
|
521
|
+
const reasons = Object.entries(result.skippedSources.reasons || {})
|
|
522
|
+
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
523
|
+
lines.push(`SKIPPED SOURCE: ${result.skippedSources.count} path(s) were not indexed${reasons ? ` (${reasons})` : ''} — completeness claims are withdrawn.`);
|
|
524
|
+
}
|
|
453
525
|
lines.push('');
|
|
454
526
|
|
|
455
|
-
const next =
|
|
456
|
-
|
|
457
|
-
|
|
527
|
+
const next = options.nextHints
|
|
528
|
+
? options.nextHints(result)
|
|
529
|
+
: [
|
|
530
|
+
...(result.suggest ? [`ucn show ${result.suggest}`] : []),
|
|
531
|
+
'ucn repo --sections=files --detailed',
|
|
532
|
+
'ucn repo --sections=stats --hot --top=20',
|
|
533
|
+
'ucn repo --sections=health --deep',
|
|
534
|
+
];
|
|
458
535
|
lines.push(`Next: ${next.join(' · ')}`);
|
|
459
536
|
|
|
460
537
|
return lines.join('\n');
|
package/core/output/search.js
CHANGED
|
@@ -7,28 +7,27 @@ const { detectDoubleEscaping, advisoryLine } = require('./shared');
|
|
|
7
7
|
/**
|
|
8
8
|
* Format search command output
|
|
9
9
|
*/
|
|
10
|
-
function formatSearch(results, term) {
|
|
10
|
+
function formatSearch(results, term, options = {}) {
|
|
11
11
|
const meta = results.meta;
|
|
12
|
-
const fallbackNote = meta && meta.regexFallback
|
|
13
|
-
? `\nNote: Invalid regex (${meta.regexFallback}). Fell back to plain text search.`
|
|
14
|
-
: '';
|
|
15
12
|
|
|
16
|
-
const
|
|
13
|
+
const shownMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
14
|
+
const totalMatches = meta?.totalMatches ?? shownMatches;
|
|
17
15
|
if (totalMatches === 0) {
|
|
18
16
|
if (meta) {
|
|
19
17
|
const scope = meta.filesSkipped > 0
|
|
20
18
|
? `Searched ${meta.filesScanned} of ${meta.totalFiles} file${meta.totalFiles === 1 ? '' : 's'} (${meta.filesSkipped} excluded by filters).`
|
|
21
19
|
: `Searched ${meta.filesScanned} file${meta.filesScanned === 1 ? '' : 's'}.`;
|
|
22
20
|
const escapingHint = detectDoubleEscaping(term);
|
|
23
|
-
return `No matches found for "${term}". ${scope}${
|
|
21
|
+
return `No matches found for "${term}". ${scope}${escapingHint}`;
|
|
24
22
|
}
|
|
25
|
-
return `No matches found for "${term}"
|
|
23
|
+
return `No matches found for "${term}"`;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
const lines = [];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const totalFiles = meta?.matchedFiles ?? meta?.totalMatchedFiles ?? results.length;
|
|
28
|
+
const fileWord = totalFiles === 1 ? 'file' : 'files';
|
|
29
|
+
const showing = shownMatches < totalMatches ? ` (showing ${shownMatches})` : '';
|
|
30
|
+
lines.push(`Found ${totalMatches} match${totalMatches === 1 ? '' : 'es'} for "${term}" in ${totalFiles} ${fileWord}${showing}:`);
|
|
32
31
|
lines.push('═'.repeat(60));
|
|
33
32
|
|
|
34
33
|
for (const result of results) {
|
|
@@ -49,11 +48,11 @@ function formatSearch(results, term) {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
if (meta && meta.truncatedMatches > 0) {
|
|
52
|
-
lines.push(`\n${
|
|
51
|
+
lines.push(`\n${shownMatches} shown of ${meta.totalMatches} total matches. ${options.topHint || 'Use --top=N to see more.'}`);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
if (meta && meta.testsExcluded && meta.filesSkipped > 0) {
|
|
56
|
-
lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (use
|
|
55
|
+
lines.push(`\nNote: ${meta.filesSkipped} test file${meta.filesSkipped === 1 ? '' : 's'} hidden by default (${options.includeTestsHint || 'use --include-tests to include'}).`);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
return lines.join('\n');
|
|
@@ -80,7 +79,6 @@ function formatSearchJson(results, term) {
|
|
|
80
79
|
obj.filesScanned = meta.filesScanned;
|
|
81
80
|
obj.filesSkipped = meta.filesSkipped;
|
|
82
81
|
obj.totalFiles = meta.totalFiles;
|
|
83
|
-
if (meta.regexFallback) obj.regexFallback = meta.regexFallback;
|
|
84
82
|
if (meta.truncatedMatches > 0) obj.truncatedMatches = meta.truncatedMatches;
|
|
85
83
|
}
|
|
86
84
|
return JSON.stringify(obj, null, 2);
|
|
@@ -89,7 +87,7 @@ function formatSearchJson(results, term) {
|
|
|
89
87
|
/**
|
|
90
88
|
* Format structural search results (index-based queries)
|
|
91
89
|
*/
|
|
92
|
-
function formatStructuralSearch(result) {
|
|
90
|
+
function formatStructuralSearch(result, options = {}) {
|
|
93
91
|
const { results, meta } = result;
|
|
94
92
|
const lines = [];
|
|
95
93
|
|
|
@@ -107,6 +105,10 @@ function formatStructuralSearch(result) {
|
|
|
107
105
|
|
|
108
106
|
lines.push(`Structural search: ${queryStr}`);
|
|
109
107
|
lines.push('═'.repeat(60));
|
|
108
|
+
if (meta.query.unused) {
|
|
109
|
+
lines.push(`NOTE: ${options.unusedFlag || '--unused'} lists callable symbols with no resolved call edge; it does not assess type/field/reference liveness and is not safe-delete proof. Confirm with deadcode and usages.`);
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
110
112
|
|
|
111
113
|
if (results.length === 0) {
|
|
112
114
|
lines.push('No matches found.');
|
|
@@ -137,7 +139,7 @@ function formatStructuralSearch(result) {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
if (meta.shown < meta.totalMatched) {
|
|
140
|
-
lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. Use top= to see more
|
|
142
|
+
lines.push(`\n${meta.shown} of ${meta.totalMatched} shown. ${options.topHint || 'Use --top=N to see more.'}`);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
return lines.join('\n');
|
|
@@ -396,8 +398,13 @@ function formatTypedefJson(types, name) {
|
|
|
396
398
|
function formatTests(tests, name) {
|
|
397
399
|
const lines = [`Tests for "${name}":\n`];
|
|
398
400
|
|
|
401
|
+
for (const warning of tests?.warnings || []) {
|
|
402
|
+
lines.push(`Note: ${warning.message}`);
|
|
403
|
+
}
|
|
404
|
+
if (tests?.warnings?.length > 0) lines.push('');
|
|
405
|
+
|
|
399
406
|
if (!tests || !Array.isArray(tests) || tests.length === 0) {
|
|
400
|
-
lines.push(' (no tests found)');
|
|
407
|
+
lines.push(' (no statically linked tests found)');
|
|
401
408
|
} else {
|
|
402
409
|
const totalMatches = tests.reduce((sum, t) => sum + t.matches.length, 0);
|
|
403
410
|
lines.push(`Found ${totalMatches} matches in ${tests.length} test file(s):\n`);
|
|
@@ -432,6 +439,7 @@ function formatTestsJson(tests, name) {
|
|
|
432
439
|
const safe = Array.isArray(tests) ? tests : [];
|
|
433
440
|
return JSON.stringify({
|
|
434
441
|
query: name,
|
|
442
|
+
...(safe.warnings?.length > 0 && { warnings: safe.warnings }),
|
|
435
443
|
testFileCount: safe.length,
|
|
436
444
|
totalMatches: safe.reduce((sum, t) => sum + (t.matches?.length || 0), 0),
|
|
437
445
|
testFiles: safe
|
package/core/output/shared.js
CHANGED
|
@@ -97,9 +97,28 @@ function formatFunctionSignature(fn) {
|
|
|
97
97
|
// Generator marker
|
|
98
98
|
if (fn.isGenerator) prefix.push('*');
|
|
99
99
|
|
|
100
|
+
// Data members are not callables. Their declared type is stronger and
|
|
101
|
+
// more useful than the unknown-parameter sentinel (UCN5-171).
|
|
102
|
+
const dataMember = fn.type === 'field' || fn.type === 'state' ||
|
|
103
|
+
fn.memberType === 'field' || fn.memberType === 'property';
|
|
104
|
+
if (dataMember) {
|
|
105
|
+
let dataSig = fn.name;
|
|
106
|
+
const declaredType = fn.fieldType || fn.returnType;
|
|
107
|
+
if (declaredType) {
|
|
108
|
+
dataSig += `: ${String(declaredType).replace(/\s+/g, ' ').trim()}`;
|
|
109
|
+
}
|
|
110
|
+
return prefix.length > 0 ? `${prefix.join(' ')} ${dataSig}` : dataSig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Object-like macros expand as values, not calls. Function-like macros
|
|
114
|
+
// retain their real parameter list, including an empty `()` (UCN5-170).
|
|
115
|
+
if (fn.type === 'macro' && fn.functionLike === false) {
|
|
116
|
+
return prefix.length > 0 ? `${prefix.join(' ')} ${fn.name}` : fn.name;
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
// Name + generics + params (concatenated without spaces)
|
|
101
120
|
let sig = fn.name;
|
|
102
|
-
if (fn.generics) sig += fn.generics;
|
|
121
|
+
if (fn.generics) sig += String(fn.generics).replace(/\s+/g, ' ').trim();
|
|
103
122
|
// If paramsStructured + paramTypes are available, render typed params
|
|
104
123
|
const typed = renderTypedParams(fn);
|
|
105
124
|
// When paramsStructured is an empty array, the function has zero params —
|
|
@@ -353,6 +372,8 @@ const ADVISORY_LABELS = {
|
|
|
353
372
|
'scored-selection': 'examples picked by usage-quality scoring',
|
|
354
373
|
'best-effort-frame-matching': 'frames matched by path similarity',
|
|
355
374
|
'heuristic-route-matching': 'route↔request matches are heuristic (see per-match EXACT/PARTIAL/UNCERTAIN tiers)',
|
|
375
|
+
'incomplete-endpoint-inventory': 'AST inventory covers known framework patterns; absence is not proof that no endpoint exists',
|
|
376
|
+
'heuristic-route-matching-and-incomplete-inventory': 'AST inventory covers known framework patterns and route↔request matches are heuristic; absence is not proof that no endpoint exists',
|
|
356
377
|
};
|
|
357
378
|
|
|
358
379
|
/** Render the advisory line for a result's `advisory` field, or null. */
|
package/core/output/tracing.js
CHANGED
|
@@ -20,7 +20,7 @@ function renderFrontier(lines, frontier, options = {}, expanded = false) {
|
|
|
20
20
|
if (!frontier || frontier.length === 0) return false;
|
|
21
21
|
lines.push('');
|
|
22
22
|
const suffix = expanded
|
|
23
|
-
?
|
|
23
|
+
? `followed (${options.expandUnverifiedHint || '--expand-unverified'}); downstream nodes are possible impact`
|
|
24
24
|
: 'not expanded; possible additional impact';
|
|
25
25
|
lines.push(`UNVERIFIED EDGES (${frontier.length}) — call syntax, no binding/receiver evidence; ${suffix}:`);
|
|
26
26
|
const cap = options.all ? Infinity : 20;
|
|
@@ -34,7 +34,7 @@ function renderFrontier(lines, frontier, options = {}, expanded = false) {
|
|
|
34
34
|
shown++;
|
|
35
35
|
}
|
|
36
36
|
if (frontier.length > shown) {
|
|
37
|
-
lines.push(` (+${frontier.length - shown} more unverified — use --all)`);
|
|
37
|
+
lines.push(` (+${frontier.length - shown} more unverified — ${options.allHint || 'use --all'})`);
|
|
38
38
|
}
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
@@ -66,8 +66,14 @@ function treeAccountLine(ta) {
|
|
|
66
66
|
`${ta.confirmedEdges} confirmed edge${ta.confirmedEdges === 1 ? '' : 's'} · ` +
|
|
67
67
|
`${ta.unverifiedEdges} unverified${reasons ? ` (${reasons})` : ''} · ` +
|
|
68
68
|
`${ta.excludedTotal} excluded${excludedReasons ? ` (${excludedReasons})` : ''}`;
|
|
69
|
+
if (ta.recursiveEdges > 0) {
|
|
70
|
+
line += ` · ${ta.recursiveEdges} self-recursive edge${ta.recursiveEdges === 1 ? '' : 's'} (not external impact)`;
|
|
71
|
+
}
|
|
69
72
|
if (ta.filteredEdges > 0) line += ` · ${ta.filteredEdges} hidden by --exclude`;
|
|
70
73
|
if (ta.depthLimitNodes > 0) line += ` · ${ta.depthLimitNodes} node${ta.depthLimitNodes === 1 ? '' : 's'} at depth limit (callers not searched)`;
|
|
74
|
+
if (ta.truncatedChildren > 0) {
|
|
75
|
+
line += ` · ${ta.truncatedChildren} child branch${ta.truncatedChildren === 1 ? '' : 'es'} not expanded (account covers the displayed subtree only)`;
|
|
76
|
+
}
|
|
71
77
|
return line;
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -317,13 +323,15 @@ function formatBlast(blast, options = {}) {
|
|
|
317
323
|
if (unverifiedEdges > 0) {
|
|
318
324
|
s += blast.expandUnverified
|
|
319
325
|
? ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} followed (${possiblyAffected || 0} possibly affected)`
|
|
320
|
-
: ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (--expand-unverified to follow them)`;
|
|
326
|
+
: ` · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (${options.expandUnverifiedHint || '--expand-unverified'} to follow them)`;
|
|
321
327
|
}
|
|
322
328
|
lines.push(s);
|
|
323
329
|
} else if (unverifiedEdges > 0) {
|
|
324
330
|
lines.push(blast.expandUnverified
|
|
325
331
|
? `Summary: no confirmed callers · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} followed (${possiblyAffected || 0} possibly affected)`
|
|
326
|
-
: `Summary: no confirmed callers · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (--expand-unverified to follow them)`);
|
|
332
|
+
: `Summary: no confirmed callers · ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (${options.expandUnverifiedHint || '--expand-unverified'} to follow them)`);
|
|
333
|
+
} else if (blast.summary.selfRecursive) {
|
|
334
|
+
lines.push('Summary: No external callers found — this function is self-recursive; entry-point status is not established by the self-edge.');
|
|
327
335
|
} else {
|
|
328
336
|
lines.push('Summary: No callers found — this function is a root/entry point.');
|
|
329
337
|
}
|
|
@@ -345,7 +353,7 @@ function formatBlast(blast, options = {}) {
|
|
|
345
353
|
|
|
346
354
|
const blastFiltered = (blast.treeAccount?.filteredEdges ?? 0);
|
|
347
355
|
if (blast.includeMethods === false && blastFiltered > 0) {
|
|
348
|
-
lines.push(`\nNote: ${blastFiltered} obj.method() caller edge(s) hidden (counted as filtered in the account). Use --include-methods to show them
|
|
356
|
+
lines.push(`\nNote: ${blastFiltered} obj.method() caller edge(s) hidden (counted as filtered in the account). ${options.includeMethodsHint || 'Use --include-methods to show them.'}`);
|
|
349
357
|
}
|
|
350
358
|
|
|
351
359
|
return lines.join('\n');
|
|
@@ -406,6 +414,8 @@ function formatReverseTrace(result, options = {}) {
|
|
|
406
414
|
}
|
|
407
415
|
if (node.entryPoint) {
|
|
408
416
|
label += ' ★ entry point';
|
|
417
|
+
} else if (node.selfRecursive && (!node.children || node.children.length === 0)) {
|
|
418
|
+
label += ' ↻ self-recursive (no external caller proven)';
|
|
409
419
|
} else if (node.unverifiedCallerCount > 0 && (!node.children || node.children.length === 0)) {
|
|
410
420
|
label += ` ⚠ no confirmed callers — ${node.unverifiedCallerCount} unverified`;
|
|
411
421
|
}
|
|
@@ -432,6 +442,8 @@ function formatReverseTrace(result, options = {}) {
|
|
|
432
442
|
let rootLabel = result.root;
|
|
433
443
|
if (result.tree && result.tree.entryPoint) {
|
|
434
444
|
rootLabel += ' ★ entry point (no callers)';
|
|
445
|
+
} else if (result.tree && result.tree.selfRecursive && result.tree.children.length === 0) {
|
|
446
|
+
rootLabel += ' ↻ self-recursive (no external caller proven)';
|
|
435
447
|
} else if (result.tree && result.tree.unverifiedCallerCount > 0 && result.tree.children.length === 0) {
|
|
436
448
|
rootLabel += ` ⚠ no confirmed callers — ${result.tree.unverifiedCallerCount} unverified`;
|
|
437
449
|
}
|
|
@@ -474,6 +486,8 @@ function formatReverseTrace(result, options = {}) {
|
|
|
474
486
|
s = `Summary: ${totalEntryPoints} entry point${totalEntryPoints !== 1 ? 's' : ''} reach${totalEntryPoints === 1 ? 'es' : ''} ${result.root}${intermediates > 0 ? ` through ${intermediates} intermediate function${intermediates !== 1 ? 's' : ''}` : ' directly'}`;
|
|
475
487
|
} else if (unverifiedEdges > 0) {
|
|
476
488
|
s = `Summary: no confirmed callers — ${unverifiedEdges} unverified edge${unverifiedEdges !== 1 ? 's' : ''} (not an entry-point claim)`;
|
|
489
|
+
} else if (result.summary.selfRecursive) {
|
|
490
|
+
s = 'Summary: No external callers found — this function is self-recursive; no entry point is proven.';
|
|
477
491
|
} else {
|
|
478
492
|
s = 'Summary: No callers found — this function is itself an entry point.';
|
|
479
493
|
}
|
|
@@ -499,7 +513,7 @@ function formatReverseTrace(result, options = {}) {
|
|
|
499
513
|
|
|
500
514
|
const rtFiltered = (result.treeAccount?.filteredEdges ?? 0);
|
|
501
515
|
if (result.includeMethods === false && rtFiltered > 0) {
|
|
502
|
-
lines.push(`\nNote: ${rtFiltered} obj.method() caller edge(s) hidden (counted as filtered in the account). Use --include-methods to show them
|
|
516
|
+
lines.push(`\nNote: ${rtFiltered} obj.method() caller edge(s) hidden (counted as filtered in the account). ${options.includeMethodsHint || 'Use --include-methods to show them.'}`);
|
|
503
517
|
}
|
|
504
518
|
|
|
505
519
|
return lines.join('\n');
|
|
@@ -531,7 +545,7 @@ function formatAffectedTests(result, options = {}) {
|
|
|
531
545
|
lines.push('');
|
|
532
546
|
|
|
533
547
|
if (result.testFiles.length === 0) {
|
|
534
|
-
lines.push('No test
|
|
548
|
+
lines.push('No confirmed static test links found for any affected function.');
|
|
535
549
|
} else {
|
|
536
550
|
const MAX_TEST_FILES = options.all ? Infinity : 30;
|
|
537
551
|
const displayFiles = result.testFiles.slice(0, MAX_TEST_FILES);
|
|
@@ -539,7 +553,7 @@ function formatAffectedTests(result, options = {}) {
|
|
|
539
553
|
lines.push(`Test files to run (${summary.totalTestFiles}):`);
|
|
540
554
|
lines.push('');
|
|
541
555
|
for (const tf of displayFiles) {
|
|
542
|
-
lines.push(` ${tf.file} (
|
|
556
|
+
lines.push(` ${tf.file} (links: ${tf.linkedFunctions.join(', ')})`);
|
|
543
557
|
// Show up to 5 key matches per file
|
|
544
558
|
const keyMatches = tf.matches
|
|
545
559
|
.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
@@ -567,25 +581,26 @@ function formatAffectedTests(result, options = {}) {
|
|
|
567
581
|
lines.push(` Additional test files (${pat.length}):`);
|
|
568
582
|
const MAX_POSSIBLE = options.all ? Infinity : 10;
|
|
569
583
|
for (const tf of pat.slice(0, MAX_POSSIBLE)) {
|
|
570
|
-
lines.push(` ${tf.file} (
|
|
584
|
+
lines.push(` ${tf.file} (links: ${tf.linkedFunctions.join(', ')})`);
|
|
571
585
|
}
|
|
572
586
|
if (pat.length > MAX_POSSIBLE) {
|
|
573
|
-
lines.push(` ... ${pat.length - MAX_POSSIBLE} more (use --all)`);
|
|
587
|
+
lines.push(` ... ${pat.length - MAX_POSSIBLE} more (${options.allHint || 'use --all'})`);
|
|
574
588
|
}
|
|
575
589
|
}
|
|
576
590
|
}
|
|
577
591
|
|
|
578
|
-
|
|
592
|
+
const notStaticallyLinked = result.notStaticallyLinked || [];
|
|
593
|
+
if (notStaticallyLinked.length > 0) {
|
|
579
594
|
lines.push('');
|
|
580
|
-
lines.push(`
|
|
581
|
-
lines.push(' ⚠
|
|
595
|
+
lines.push(`Not statically linked (${notStaticallyLinked.length}): ${notStaticallyLinked.join(', ')}`);
|
|
596
|
+
lines.push(' ⚠ No indexed test call/reference path was found; this is not runtime coverage data');
|
|
582
597
|
}
|
|
583
598
|
|
|
584
599
|
lines.push('');
|
|
585
600
|
const pct = summary.totalAffected > 0
|
|
586
|
-
? Math.round(summary.
|
|
601
|
+
? Math.round(summary.staticallyLinkedFunctions / summary.totalAffected * 100)
|
|
587
602
|
: 0;
|
|
588
|
-
let summaryLine = `Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} test files, ${summary.
|
|
603
|
+
let summaryLine = `Summary: ${summary.totalAffected} affected → ${summary.totalTestFiles} statically linked test files, ${summary.staticallyLinkedFunctions}/${summary.totalAffected} functions linked (${pct}%)`;
|
|
589
604
|
if (summary.possiblyAffected > 0) {
|
|
590
605
|
summaryLine += ` · ${summary.possiblyAffected} possibly affected (unverified chains)`;
|
|
591
606
|
}
|