ucn 4.2.2 → 5.0.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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- 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 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- 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 +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- 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 +216 -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 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- 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 +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/output/analysis.js
CHANGED
|
@@ -88,17 +88,34 @@ function formatAccountLines(account) {
|
|
|
88
88
|
`${account.confirmed} confirmed, ${account.unverified} unverified, ` +
|
|
89
89
|
`${nc.total} non-call (${nc.imports} import, ${nc.definitions} definition, ${nc.references} reference, ${nc.unclassifiedText} other-text), ` +
|
|
90
90
|
`${account.excluded ? account.excluded.total : 0} other-target, ` +
|
|
91
|
+
`${account.unsupported && account.unsupported.lines > 0 ? `${account.unsupported.lines} unsupported-language, ` : ''}` +
|
|
91
92
|
`${account.unaccounted} unaccounted`;
|
|
92
93
|
if (account.beyondText && account.beyondText.count > 0) {
|
|
93
94
|
line += ` (+${account.beyondText.count} beyond-text caller${account.beyondText.count === 1 ? '' : 's'})`;
|
|
94
95
|
}
|
|
95
96
|
lines.push(line);
|
|
96
97
|
const contract = account.contract || {};
|
|
98
|
+
const unsupported = account.unsupported ||
|
|
99
|
+
{ fileCount: 0, lines: 0, files: [], languages: {}, sites: [], sitesTruncated: false };
|
|
97
100
|
const textComplete = contract.textComplete !== undefined
|
|
98
101
|
? contract.textComplete
|
|
99
102
|
: Boolean(account.conserved) && !(account.unparsed && account.unparsed.fileCount > 0) &&
|
|
103
|
+
unsupported.lines === 0 &&
|
|
100
104
|
!(account.unreadableFiles && account.unreadableFiles.length > 0);
|
|
101
|
-
|
|
105
|
+
// When the ONLY gap is unsupported-language occurrences, say exactly that
|
|
106
|
+
// instead of the generic DEGRADED sentence: the partition over supported
|
|
107
|
+
// languages is still conserved, and the gap is enumerated below.
|
|
108
|
+
const unsupportedOnly = !textComplete && unsupported.lines > 0 &&
|
|
109
|
+
Boolean(account.conserved) &&
|
|
110
|
+
!(account.unparsed && account.unparsed.fileCount > 0) &&
|
|
111
|
+
!(account.unreadableFiles && account.unreadableFiles.length > 0);
|
|
112
|
+
if (unsupportedOnly) {
|
|
113
|
+
const langs = Object.keys(unsupported.languages).join(', ');
|
|
114
|
+
lines.push(`CONTRACT: literal-name text partition complete over SUPPORTED languages only — ` +
|
|
115
|
+
`${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in unsupported-language file${unsupported.fileCount === 1 ? '' : 's'}` +
|
|
116
|
+
`${langs ? ` (${langs})` : ''} NOT analyzed (see WARNING; verify with grep/ripgrep). ` +
|
|
117
|
+
'Semantic completeness is not claimed.');
|
|
118
|
+
} else if (!textComplete) {
|
|
102
119
|
lines.push('CONTRACT: literal-name text partition is DEGRADED; review WARNING and unaccounted lines before acting. Semantic completeness is not claimed.');
|
|
103
120
|
} else if (contract.observedTextZero) {
|
|
104
121
|
lines.push('CONTRACT: observed-text zero only; every literal-name line was classified, but aliases, indirect calls, generated code, and runtime dispatch may exist. Not safe-delete proof.');
|
|
@@ -111,8 +128,31 @@ function formatAccountLines(account) {
|
|
|
111
128
|
`(${account.unparsed.lines} line${account.unparsed.lines === 1 ? '' : 's'}, NOT analyzed): ` +
|
|
112
129
|
account.unparsed.files.join(', '));
|
|
113
130
|
}
|
|
131
|
+
if (unsupported.lines > 0) {
|
|
132
|
+
const langParts = Object.entries(unsupported.languages)
|
|
133
|
+
.map(([lang, count]) => `${lang}: ${count}`).join(', ');
|
|
134
|
+
lines.push(`WARNING: ${unsupported.lines} line${unsupported.lines === 1 ? '' : 's'} in ` +
|
|
135
|
+
`${unsupported.fileCount} unsupported-language file${unsupported.fileCount === 1 ? '' : 's'} ` +
|
|
136
|
+
`contain${unsupported.lines === 1 ? 's' : ''} "${account.symbol}"` +
|
|
137
|
+
`${langParts ? ` (${langParts})` : ''} — NOT analyzed; verify with grep/ripgrep:`);
|
|
138
|
+
const shown = unsupported.sites.slice(0, 5);
|
|
139
|
+
for (const site of shown) {
|
|
140
|
+
lines.push(` ${site.file}:${site.line} ${site.text}`);
|
|
141
|
+
}
|
|
142
|
+
if (unsupported.lines > shown.length) {
|
|
143
|
+
lines.push(` ... and ${unsupported.lines - shown.length} more line${unsupported.lines - shown.length === 1 ? '' : 's'}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
114
146
|
if (account.unreadableFiles && account.unreadableFiles.length > 0) {
|
|
115
|
-
lines.push(`WARNING: ${account.unreadableFiles.length}
|
|
147
|
+
lines.push(`WARNING: ${account.unreadableFiles.length} unreadable file(s) skipped: ${account.unreadableFiles.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
if (account.skippedSources && account.skippedSources.length > 0) {
|
|
150
|
+
const shown = account.skippedSources.slice(0, 5)
|
|
151
|
+
.map(issue => `${issue.relativePath} (${issue.reason})`);
|
|
152
|
+
lines.push(`WARNING: ${account.skippedSources.length} source discovery gap(s) make this index partial: ${shown.join(', ')}`);
|
|
153
|
+
if (account.skippedSources.length > shown.length) {
|
|
154
|
+
lines.push(` ... and ${account.skippedSources.length - shown.length} more; run repo --sections=health for the full reason set.`);
|
|
155
|
+
}
|
|
116
156
|
}
|
|
117
157
|
if (account.filtered && account.filtered.total > 0) {
|
|
118
158
|
const parts = [];
|
|
@@ -156,11 +196,18 @@ function unverifiedCalleeLines(entries, compact) {
|
|
|
156
196
|
}
|
|
157
197
|
|
|
158
198
|
/** "NON-CALL OCCURRENCES" summary line from the account. */
|
|
159
|
-
function formatNonCallLine(account, hintName) {
|
|
199
|
+
function formatNonCallLine(account, hintName, usagesHint) {
|
|
160
200
|
if (!account || !account.nonCall || account.nonCall.total === 0) return null;
|
|
161
201
|
const nc = account.nonCall;
|
|
202
|
+
const hint = usagesHint || `ucn usages ${hintName}`;
|
|
203
|
+
// Classification differs across the two surfaces by contract: these
|
|
204
|
+
// counts are engine-adjudicated (a method reference that resolves to
|
|
205
|
+
// this target is a confirmed caller, not a reference), while `usages`
|
|
206
|
+
// classifies each line by syntax alone — the breakdowns can
|
|
207
|
+
// legitimately differ.
|
|
162
208
|
return `NON-CALL OCCURRENCES: ${nc.total} (${nc.imports} imports, ${nc.definitions} definitions, ` +
|
|
163
|
-
`${nc.references} references, ${nc.unclassifiedText} other-text) — counts
|
|
209
|
+
`${nc.references} references, ${nc.unclassifiedText} other-text) — engine-adjudicated counts; ` +
|
|
210
|
+
`syntax-classified line records: ${hint}`;
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
/** Format context (callers + callees) as JSON */
|
|
@@ -191,8 +238,11 @@ function formatContextJson(context) {
|
|
|
191
238
|
usages: callers.map(c => ({
|
|
192
239
|
file: c.relativePath || c.file,
|
|
193
240
|
line: c.line,
|
|
241
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
194
242
|
expression: c.content,
|
|
195
243
|
callerName: c.callerName,
|
|
244
|
+
...(c.calledAs && { calledAs: c.calledAs }),
|
|
245
|
+
...(c.isFunctionReference && { functionReference: true }),
|
|
196
246
|
// Tier parity with the function-path callers list: class
|
|
197
247
|
// usages are the confirmed-tier answer for type symbols.
|
|
198
248
|
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
@@ -204,8 +254,11 @@ function formatContextJson(context) {
|
|
|
204
254
|
unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
|
|
205
255
|
file: c.relativePath || c.file,
|
|
206
256
|
line: c.line,
|
|
257
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
207
258
|
expression: c.content,
|
|
208
259
|
callerName: c.callerName ?? null,
|
|
260
|
+
...(c.calledAs && { calledAs: c.calledAs }),
|
|
261
|
+
...(c.isFunctionReference && { functionReference: true }),
|
|
209
262
|
tier: 'unverified',
|
|
210
263
|
...(c.confidence !== undefined && { confidence: c.confidence }),
|
|
211
264
|
...(c.evidenceScore !== undefined && { evidenceScore: c.evidenceScore }),
|
|
@@ -215,6 +268,10 @@ function formatContextJson(context) {
|
|
|
215
268
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
216
269
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
217
270
|
...(c.externalContract && { externalContract: true }),
|
|
271
|
+
uncertaintyClass: c.uncertaintyClass ||
|
|
272
|
+
(c.reason === 'possible-dispatch' && c.dispatchVia
|
|
273
|
+
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
274
|
+
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
218
275
|
})),
|
|
219
276
|
...(context.warnings && { warnings: context.warnings })
|
|
220
277
|
}
|
|
@@ -238,6 +295,7 @@ function formatContextJson(context) {
|
|
|
238
295
|
callers: callers.map(c => ({
|
|
239
296
|
file: c.relativePath || c.file,
|
|
240
297
|
line: c.line,
|
|
298
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
241
299
|
expression: c.content, // FULL expression
|
|
242
300
|
callerName: c.callerName,
|
|
243
301
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -251,6 +309,7 @@ function formatContextJson(context) {
|
|
|
251
309
|
unverifiedCallers: unverifiedCallers.map(c => ({
|
|
252
310
|
file: c.relativePath || c.file,
|
|
253
311
|
line: c.line,
|
|
312
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
254
313
|
expression: c.content, // FULL expression
|
|
255
314
|
callerName: c.callerName ?? null,
|
|
256
315
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -263,6 +322,10 @@ function formatContextJson(context) {
|
|
|
263
322
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
264
323
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
265
324
|
...(c.externalContract && { externalContract: true }),
|
|
325
|
+
uncertaintyClass: c.uncertaintyClass ||
|
|
326
|
+
(c.reason === 'possible-dispatch' && c.dispatchVia
|
|
327
|
+
? 'runtime-dispatch' : 'actionable-ambiguity'),
|
|
328
|
+
...(c.dispatchFamily && { dispatchFamily: c.dispatchFamily }),
|
|
266
329
|
})),
|
|
267
330
|
callees: callees.map(c => ({
|
|
268
331
|
name: c.name,
|
|
@@ -295,13 +358,14 @@ function formatContext(ctx, options = {}) {
|
|
|
295
358
|
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
296
359
|
|
|
297
360
|
const expandHint = options.expandHint != null ? options.expandHint : 'Use ucn_expand with item number to see code for any item.';
|
|
361
|
+
const allHint = options.allHint || 'use --all';
|
|
298
362
|
|
|
299
363
|
const lines = [];
|
|
300
364
|
const expandable = [];
|
|
301
365
|
let itemNum = 1;
|
|
302
366
|
|
|
303
367
|
// Handle struct/interface types
|
|
304
|
-
if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
|
|
368
|
+
if (ctx.type && ['class', 'struct', 'interface', 'type', 'enum', 'record', 'trait', 'namespace'].includes(ctx.type)) {
|
|
305
369
|
lines.push(`Context for ${ctx.type} ${ctx.name}:`);
|
|
306
370
|
lines.push('═'.repeat(60));
|
|
307
371
|
|
|
@@ -330,8 +394,20 @@ function formatContext(ctx, options = {}) {
|
|
|
330
394
|
});
|
|
331
395
|
}
|
|
332
396
|
|
|
397
|
+
const members = ctx.members || [];
|
|
398
|
+
if (members.length > 0) {
|
|
399
|
+
lines.push(`\nMEMBERS (${members.length}):`);
|
|
400
|
+
for (const member of members) {
|
|
401
|
+
lines.push(` ${member.name} (${member.type})`);
|
|
402
|
+
lines.push(` ${member.file}:${member.line}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
333
406
|
const callers = ctx.callers || [];
|
|
334
|
-
|
|
407
|
+
const typeCallerTotal = ctx.meta?.callerTotal ?? callers.length;
|
|
408
|
+
const typeCallerLabel = typeCallerTotal > callers.length
|
|
409
|
+
? `${callers.length} shown of ${typeCallerTotal}` : `${callers.length}`;
|
|
410
|
+
lines.push(`\nCALLERS — CONFIRMED (${typeCallerLabel}):`);
|
|
335
411
|
for (const c of callers) {
|
|
336
412
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
337
413
|
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
@@ -372,7 +448,7 @@ function formatContext(ctx, options = {}) {
|
|
|
372
448
|
shown++;
|
|
373
449
|
}
|
|
374
450
|
if (typeUnverified.length > shown) {
|
|
375
|
-
lines.push(` (+${typeUnverified.length - shown} more unverified —
|
|
451
|
+
lines.push(` (+${typeUnverified.length - shown} more unverified — ${allHint})`);
|
|
376
452
|
}
|
|
377
453
|
}
|
|
378
454
|
|
|
@@ -420,9 +496,12 @@ function formatContext(ctx, options = {}) {
|
|
|
420
496
|
const callers = ctx.callers || [];
|
|
421
497
|
const prodCallers = callers.filter(c => !isTestEntry(c));
|
|
422
498
|
const testCallers = callers.filter(c => isTestEntry(c));
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
499
|
+
const callerTotal = ctx.meta?.callerTotal ?? callers.length;
|
|
500
|
+
const tierHeader = callerTotal > callers.length
|
|
501
|
+
? `CALLERS — CONFIRMED (${callers.length} shown of ${callerTotal}):`
|
|
502
|
+
: testCallers.length > 0
|
|
503
|
+
? `CALLERS — CONFIRMED (${callers.length}, ${prodCallers.length} prod + ${testCallers.length} test):`
|
|
504
|
+
: `CALLERS — CONFIRMED (${callers.length}):`;
|
|
426
505
|
lines.push(`${compact ? '' : '\n'}${tierHeader}`);
|
|
427
506
|
const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
|
|
428
507
|
if (callerEvidence) lines.push(callerEvidence);
|
|
@@ -460,15 +539,123 @@ function formatContext(ctx, options = {}) {
|
|
|
460
539
|
// candidates ARE analyzed — they render in the unverified band with
|
|
461
540
|
// reasons, and the ACCOUNT line reconciles every text occurrence.
|
|
462
541
|
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
542
|
+
// Runtime dispatch has a known boundary (trait/interface/supertype or a
|
|
543
|
+
// capability guard), but the concrete implementation is selected at
|
|
544
|
+
// runtime. Present repeated sites as one decision family with samples:
|
|
545
|
+
// the raw sites remain in JSON/accounting and --all expands every one.
|
|
546
|
+
// This separates unavoidable language semantics from ambiguity the agent
|
|
547
|
+
// can investigate.
|
|
466
548
|
const unverified = ctx.unverifiedCallers || [];
|
|
467
|
-
|
|
468
|
-
|
|
549
|
+
const runtimeDispatch = unverified.filter(u =>
|
|
550
|
+
u.uncertaintyClass === 'runtime-dispatch' ||
|
|
551
|
+
(u.reason === 'possible-dispatch' && u.dispatchVia));
|
|
552
|
+
const compileTimeDispatch = unverified.filter(u =>
|
|
553
|
+
u.uncertaintyClass === 'compile-time-dispatch');
|
|
554
|
+
const actionableUnverified = unverified.filter(u =>
|
|
555
|
+
u.uncertaintyClass !== 'compile-time-dispatch' &&
|
|
556
|
+
!(u.uncertaintyClass === 'runtime-dispatch' ||
|
|
557
|
+
(u.reason === 'possible-dispatch' && u.dispatchVia)));
|
|
558
|
+
if (runtimeDispatch.length > 0) {
|
|
559
|
+
const groups = new Map();
|
|
560
|
+
for (const site of runtimeDispatch) {
|
|
561
|
+
const key = `${site.dispatchVia}\0${site.dispatchCandidates ?? ''}\0` +
|
|
562
|
+
`${site.externalContract ? 'external' : 'project'}`;
|
|
563
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
564
|
+
groups.get(key).push(site);
|
|
565
|
+
}
|
|
566
|
+
lines.push(`${compact ? '' : '\n'}CALLERS — RUNTIME DISPATCH ` +
|
|
567
|
+
`(${runtimeDispatch.length} site${runtimeDispatch.length === 1 ? '' : 's'}, ` +
|
|
568
|
+
`${groups.size} ${groups.size === 1 ? 'family' : 'families'}) — ` +
|
|
569
|
+
'implementation selected at runtime:');
|
|
570
|
+
const showAll = !!ctx.meta?.all;
|
|
571
|
+
let summarized = 0;
|
|
572
|
+
for (const sites of groups.values()) {
|
|
573
|
+
const first = sites[0];
|
|
574
|
+
const implementation = first.dispatchCandidates > 1
|
|
575
|
+
? `; 1 of ${first.dispatchCandidates} implementations`
|
|
576
|
+
: first.externalContract ? '; open external implementation set' : '';
|
|
577
|
+
lines.push(` via ${first.dispatchVia}: ${sites.length} ` +
|
|
578
|
+
`site${sites.length === 1 ? '' : 's'}${implementation}`);
|
|
579
|
+
const samples = showAll ? sites : sites.slice(0, 2);
|
|
580
|
+
for (const u of samples) {
|
|
581
|
+
const callerName = u.callerName ? ` [${u.callerName}]` : '';
|
|
582
|
+
const expr = u.content
|
|
583
|
+
? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
584
|
+
lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}`);
|
|
585
|
+
expandable.push({
|
|
586
|
+
num: itemNum++,
|
|
587
|
+
type: 'caller',
|
|
588
|
+
name: u.callerName || '(module level)',
|
|
589
|
+
file: u.callerFile || u.file,
|
|
590
|
+
relativePath: u.relativePath,
|
|
591
|
+
line: u.line,
|
|
592
|
+
startLine: u.callerStartLine || u.line,
|
|
593
|
+
endLine: u.callerEndLine || u.line
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
summarized += sites.length - samples.length;
|
|
597
|
+
}
|
|
598
|
+
if (summarized > 0) {
|
|
599
|
+
lines.push(` (+${summarized} more runtime-dispatch sites — ${allHint})`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Template substitution, constraints, and dependent calls are selected by
|
|
604
|
+
// the C++ compiler. Repeating every source site as generic actionable
|
|
605
|
+
// ambiguity makes an agent inspect code it cannot settle statically. Keep
|
|
606
|
+
// every raw site in JSON/accounting and group the text view by the exact
|
|
607
|
+
// overload family that needs a compiler handoff.
|
|
608
|
+
if (compileTimeDispatch.length > 0) {
|
|
609
|
+
const groups = new Map();
|
|
610
|
+
for (const site of compileTimeDispatch) {
|
|
611
|
+
const key = `${site.dispatchFamily || 'template overload set'}\0` +
|
|
612
|
+
`${site.dispatchCandidates ?? ''}`;
|
|
613
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
614
|
+
groups.get(key).push(site);
|
|
615
|
+
}
|
|
616
|
+
lines.push(`${compact ? '' : '\n'}CALLERS — COMPILE-TIME DISPATCH ` +
|
|
617
|
+
`(${compileTimeDispatch.length} site${compileTimeDispatch.length === 1 ? '' : 's'}, ` +
|
|
618
|
+
`${groups.size} ${groups.size === 1 ? 'family' : 'families'}) — ` +
|
|
619
|
+
'exact overload depends on template substitution or constraints:');
|
|
620
|
+
const showAll = !!ctx.meta?.all;
|
|
621
|
+
let summarized = 0;
|
|
622
|
+
for (const sites of groups.values()) {
|
|
623
|
+
const first = sites[0];
|
|
624
|
+
const candidates = first.dispatchCandidates > 1
|
|
625
|
+
? `; ${first.dispatchCandidates} candidate overloads` : '';
|
|
626
|
+
lines.push(` ${first.dispatchFamily || 'template overload set'}: ` +
|
|
627
|
+
`${sites.length} site${sites.length === 1 ? '' : 's'}${candidates}`);
|
|
628
|
+
const samples = showAll ? sites : sites.slice(0, 2);
|
|
629
|
+
for (const u of samples) {
|
|
630
|
+
const callerName = u.callerName ? ` [${u.callerName}]` : '';
|
|
631
|
+
const expr = u.content
|
|
632
|
+
? `: ${u.content.trim().replace(/\s+/g, ' ').slice(0, 100)}` : '';
|
|
633
|
+
lines.push(` [${itemNum}] ${u.relativePath}:${u.line}${callerName}${expr}`);
|
|
634
|
+
expandable.push({
|
|
635
|
+
num: itemNum++,
|
|
636
|
+
type: 'caller',
|
|
637
|
+
name: u.callerName || '(module level)',
|
|
638
|
+
file: u.callerFile || u.file,
|
|
639
|
+
relativePath: u.relativePath,
|
|
640
|
+
line: u.line,
|
|
641
|
+
startLine: u.callerStartLine || u.line,
|
|
642
|
+
endLine: u.callerEndLine || u.line
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
summarized += sites.length - samples.length;
|
|
646
|
+
}
|
|
647
|
+
if (summarized > 0) {
|
|
648
|
+
lines.push(` (+${summarized} more compile-time-dispatch sites — ${allHint})`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Actionable ambiguity: call syntax without enough identity evidence.
|
|
653
|
+
// Always visible and capped at 10 one-liners unless --all.
|
|
654
|
+
if (actionableUnverified.length > 0) {
|
|
655
|
+
lines.push(`${compact ? '' : '\n'}CALLERS — UNVERIFIED (${actionableUnverified.length}) — call syntax, no binding/receiver evidence:`);
|
|
469
656
|
const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
|
|
470
657
|
let shown = 0;
|
|
471
|
-
for (const u of
|
|
658
|
+
for (const u of actionableUnverified) {
|
|
472
659
|
if (shown >= cap) break;
|
|
473
660
|
const callerName = u.callerName ? ` [${u.callerName}]` : '';
|
|
474
661
|
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
@@ -486,13 +673,16 @@ function formatContext(ctx, options = {}) {
|
|
|
486
673
|
});
|
|
487
674
|
shown++;
|
|
488
675
|
}
|
|
489
|
-
if (
|
|
490
|
-
lines.push(` (+${
|
|
676
|
+
if (actionableUnverified.length > shown) {
|
|
677
|
+
lines.push(` (+${actionableUnverified.length - shown} more unverified — ${allHint})`);
|
|
491
678
|
}
|
|
492
679
|
}
|
|
493
680
|
|
|
494
681
|
const callees = ctx.callees || [];
|
|
495
|
-
|
|
682
|
+
const calleeTotal = ctx.meta?.calleeTotal ?? callees.length;
|
|
683
|
+
const calleeLabel = calleeTotal > callees.length
|
|
684
|
+
? `${callees.length} shown of ${calleeTotal}` : `${callees.length}`;
|
|
685
|
+
lines.push(`${compact ? '' : '\n'}CALLEES (${calleeLabel}):`);
|
|
496
686
|
const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
|
|
497
687
|
if (calleeEvidence) lines.push(calleeEvidence);
|
|
498
688
|
const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
|
|
@@ -531,7 +721,7 @@ function formatContext(ctx, options = {}) {
|
|
|
531
721
|
// Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
|
|
532
722
|
const account = ctx.meta && ctx.meta.account;
|
|
533
723
|
if (account) {
|
|
534
|
-
const nonCallLine = formatNonCallLine(account, ctx.function);
|
|
724
|
+
const nonCallLine = formatNonCallLine(account, ctx.function, options.usagesHint);
|
|
535
725
|
if (nonCallLine) lines.push(`${compact ? '' : '\n'}${nonCallLine}`);
|
|
536
726
|
const accountLines = formatAccountLines(account);
|
|
537
727
|
if (accountLines.length > 0) {
|
|
@@ -563,6 +753,9 @@ function formatImpact(impact, options = {}) {
|
|
|
563
753
|
if (!compact) lines.push('═'.repeat(60));
|
|
564
754
|
lines.push(`${impact.file}:${impact.startLine}`);
|
|
565
755
|
if (!compact) lines.push(impact.signature);
|
|
756
|
+
for (const warning of impact.warnings || []) {
|
|
757
|
+
lines.push(`Note: ${warning.message}`);
|
|
758
|
+
}
|
|
566
759
|
if (!compact) lines.push('');
|
|
567
760
|
|
|
568
761
|
// Summary (confirmed + unverified tiers reported separately)
|
|
@@ -689,6 +882,7 @@ function formatAbout(about, options = {}) {
|
|
|
689
882
|
}
|
|
690
883
|
|
|
691
884
|
const lines = [];
|
|
885
|
+
const allHint = options.allHint || 'use --all';
|
|
692
886
|
const sym = about.symbol;
|
|
693
887
|
const { expand, root, depth } = options;
|
|
694
888
|
|
|
@@ -805,7 +999,7 @@ function formatAbout(about, options = {}) {
|
|
|
805
999
|
lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
|
|
806
1000
|
}
|
|
807
1001
|
if (aboutUnverified.total > aboutUnverified.top.length) {
|
|
808
|
-
lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified —
|
|
1002
|
+
lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — ${allHint})`);
|
|
809
1003
|
}
|
|
810
1004
|
}
|
|
811
1005
|
|
|
@@ -923,7 +1117,7 @@ function formatAbout(about, options = {}) {
|
|
|
923
1117
|
lines.push(about.code);
|
|
924
1118
|
} else if (about.code && compact) {
|
|
925
1119
|
lines.push('');
|
|
926
|
-
lines.push(`SOURCE: omitted in compact mode; use
|
|
1120
|
+
lines.push(`SOURCE: omitted in compact mode; use source ${sym.handle || sym.name} to extract it.`);
|
|
927
1121
|
}
|
|
928
1122
|
|
|
929
1123
|
if (aboutTruncated) {
|
package/core/output/brief.js
CHANGED
|
@@ -14,6 +14,18 @@ function signatureLine(sym) {
|
|
|
14
14
|
const parts = [];
|
|
15
15
|
if (sym.modifiers && sym.modifiers.length) parts.push(sym.modifiers.join(' '));
|
|
16
16
|
let sig = sym.name;
|
|
17
|
+
if (sym.generics) sig += String(sym.generics).replace(/\s+/g, ' ').trim();
|
|
18
|
+
if (sym.type === 'field' || sym.type === 'state' ||
|
|
19
|
+
sym.memberType === 'field' || sym.memberType === 'property') {
|
|
20
|
+
const declaredType = sym.fieldType || sym.returnType;
|
|
21
|
+
if (declaredType) sig += `: ${String(declaredType).replace(/\s+/g, ' ').trim()}`;
|
|
22
|
+
parts.push(sig);
|
|
23
|
+
return parts.join(' ');
|
|
24
|
+
}
|
|
25
|
+
if (sym.type === 'macro' && sym.functionLike === false) {
|
|
26
|
+
parts.push(sig);
|
|
27
|
+
return parts.join(' ');
|
|
28
|
+
}
|
|
17
29
|
const typed = renderTypedParams(sym);
|
|
18
30
|
// If we have a structured-params array of length 0, the function has no params.
|
|
19
31
|
// Render `()` rather than the legacy `(...)` placeholder.
|
|
@@ -57,6 +69,17 @@ function formatBrief(result) {
|
|
|
57
69
|
return lines.join('\n');
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
if (result.kind === 'data') {
|
|
73
|
+
lines.push(signatureLine(sym));
|
|
74
|
+
lines.push(` ${sym.file}:${sym.startLine}-${sym.endLine} (${result.lineCount || 0} line${result.lineCount === 1 ? '' : 's'})`);
|
|
75
|
+
if (sym.handle) lines.push(` handle: ${sym.handle}`);
|
|
76
|
+
if (sym.docstring) lines.push(` "${sym.docstring}"`);
|
|
77
|
+
if (sym.className) lines.push(` in class ${sym.className}`);
|
|
78
|
+
const gitLineData = formatGitLine(result.git);
|
|
79
|
+
if (gitLineData) lines.push(` ${gitLineData}`);
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
// Header line: signature
|
|
61
84
|
lines.push(signatureLine(sym));
|
|
62
85
|
// Location + line count
|
package/core/output/check.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
function formatCheck(result) {
|
|
8
8
|
if (!result) return 'No check result.';
|
|
9
|
+
if (result.ok === false) {
|
|
10
|
+
// The gate could not run — never render this like a clean tree.
|
|
11
|
+
return `Pre-commit Check (${result.base}${result.staged ? ', staged' : ''})\n${'═'.repeat(60)}\nCHECK DID NOT RUN [${result.status || 'diff-failed'}] — ${result.error || 'git diff failed'}\nThis is not a pass. Fix the git context (run inside a git repository with a valid base ref) and rerun.`;
|
|
12
|
+
}
|
|
9
13
|
if (result.empty) {
|
|
10
14
|
return `Pre-commit Check (${result.base}${result.staged ? ', staged' : ''})\n${'═'.repeat(60)}\nNo changes to analyze${result.reason ? ` (${result.reason})` : ''}.`;
|
|
11
15
|
}
|
package/core/output/doctor.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const { langTraits } = require('../../languages');
|
|
8
|
+
|
|
9
|
+
function formatDoctor(result, options = {}) {
|
|
8
10
|
if (!result) return 'No project to analyze.';
|
|
9
11
|
const lines = [];
|
|
10
12
|
lines.push(`UCN Trust Report — ${result.root}`);
|
|
@@ -12,14 +14,20 @@ function formatDoctor(result) {
|
|
|
12
14
|
if (result.version) lines.push(`Version: ucn ${result.version}`);
|
|
13
15
|
lines.push(`Index: ${result.files.scanned} file${result.files.scanned === 1 ? '' : 's'}, ${result.symbols} symbol${result.symbols === 1 ? '' : 's'}`);
|
|
14
16
|
|
|
15
|
-
if (result.filter)
|
|
17
|
+
if (result.filter) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
if (result.filter.file) parts.push(`file=${result.filter.file}`);
|
|
20
|
+
if (result.filter.in) parts.push(`in=${result.filter.in}`);
|
|
21
|
+
if (result.filter.exclude?.length) parts.push(`exclude=${result.filter.exclude.join(',')}`);
|
|
22
|
+
if (parts.length > 0) lines.push(`Filter: ${parts.join(' · ')}`);
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
// Languages
|
|
18
26
|
const langEntries = Object.entries(result.languages || {}).sort((a, b) => b[1].files - a[1].files);
|
|
19
27
|
if (langEntries.length) {
|
|
20
28
|
const totalFiles = langEntries.reduce((s, [, v]) => s + v.files, 0) || 1;
|
|
21
29
|
const langStr = langEntries.map(([name, v]) => `${name} (${Math.round(v.files / totalFiles * 100)}%)`).join(', ');
|
|
22
|
-
lines.push(`Languages: ${langStr}`);
|
|
30
|
+
lines.push(`Languages (by indexed files): ${langStr}`);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
// Cache state
|
|
@@ -52,23 +60,35 @@ function formatDoctor(result) {
|
|
|
52
60
|
lines.push('Resolution evidence profile: no caller edges in the stratified sample.');
|
|
53
61
|
} else {
|
|
54
62
|
lines.push('');
|
|
55
|
-
lines.push(
|
|
63
|
+
lines.push(`Resolution evidence profile: not computed (${options.deepHint || 'use --deep'})`);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Blind spots
|
|
59
67
|
lines.push('');
|
|
60
68
|
lines.push('Blind spots:');
|
|
61
69
|
const bs = result.blindSpots || {};
|
|
70
|
+
const staticSpecialImports = result.projectLanguage &&
|
|
71
|
+
!langTraits(result.projectLanguage)?.hasDynamicImports;
|
|
72
|
+
const importLabel = staticSpecialImports
|
|
73
|
+
? (result.projectLanguage === 'rust' ? 'Glob imports' : 'Blank/dot imports')
|
|
74
|
+
: 'Dynamic imports';
|
|
62
75
|
const bsLines = [
|
|
63
|
-
[
|
|
76
|
+
[importLabel, bs.dynamicImports],
|
|
64
77
|
['Eval/exec calls', bs.evalCalls],
|
|
65
78
|
['Reflection', bs.reflection],
|
|
79
|
+
['Computed dispatch', bs.computedDispatch],
|
|
66
80
|
['Parse failures', bs.parseFailures],
|
|
67
81
|
['Parser recovery', bs.parseRecoveries],
|
|
82
|
+
['Unsupported source', bs.unsupportedSources],
|
|
83
|
+
['Skipped source', bs.skippedSources],
|
|
68
84
|
];
|
|
69
85
|
const unitFor = {
|
|
70
|
-
'Dynamic imports': 'import', '
|
|
86
|
+
'Dynamic imports': 'import', 'Blank/dot imports': 'import',
|
|
87
|
+
'Glob imports': 'import', 'Eval/exec calls': 'use', Reflection: 'use',
|
|
88
|
+
'Computed dispatch': 'call',
|
|
71
89
|
'Parse failures': 'failure',
|
|
90
|
+
'Unsupported source': 'file',
|
|
91
|
+
'Skipped source': 'path',
|
|
72
92
|
};
|
|
73
93
|
let anyBlindSpot = false;
|
|
74
94
|
for (const [label, info] of bsLines) {
|
|
@@ -82,6 +102,17 @@ function formatDoctor(result) {
|
|
|
82
102
|
const unit = unitFor[label] || 'use';
|
|
83
103
|
if (label === 'Parser recovery') {
|
|
84
104
|
lines.push(` ${label}: ${fileCount} recovered file${fileCount === 1 ? '' : 's'} (results may be partial)`);
|
|
105
|
+
} else if (label === 'Unsupported source') {
|
|
106
|
+
const languages = Object.entries(info.languages || {})
|
|
107
|
+
.map(([language, count]) => `${language} ${count}`)
|
|
108
|
+
.join(', ');
|
|
109
|
+
lines.push(` ${label}: ${fileCount} file${fileCount === 1 ? '' : 's'} skipped${languages ? ` (${languages})` : ''}`);
|
|
110
|
+
lines.push(' Handoff: use grep/ripgrep and a language-native analyzer for these files.');
|
|
111
|
+
} else if (label === 'Skipped source') {
|
|
112
|
+
const reasons = Object.entries(info.reasons || {})
|
|
113
|
+
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
114
|
+
lines.push(` ${label}: ${fileCount} path${fileCount === 1 ? '' : 's'} not indexed${reasons ? ` (${reasons})` : ''}`);
|
|
115
|
+
lines.push(' The index is partial; adjust .ucn.json or discovery limits before relying on completeness-sensitive commands.');
|
|
85
116
|
} else {
|
|
86
117
|
lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
87
118
|
}
|
package/core/output/endpoints.js
CHANGED
|
@@ -26,7 +26,7 @@ function formatEndpoints(result, options = {}) {
|
|
|
26
26
|
const showBridge = options.bridge;
|
|
27
27
|
|
|
28
28
|
if (!showBridge) {
|
|
29
|
-
return formatRoutesAndRequests(routes, requests, meta, options);
|
|
29
|
+
return formatRoutesAndRequests(routes, requests, meta, options, result.advisory);
|
|
30
30
|
}
|
|
31
31
|
return formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, options, result.advisory);
|
|
32
32
|
}
|
|
@@ -48,7 +48,7 @@ function uniqueMatchPercent(bridges, totalRequests) {
|
|
|
48
48
|
return Math.min(100, Math.max(0, pct));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function formatRoutesAndRequests(routes, requests, meta, options) {
|
|
51
|
+
function formatRoutesAndRequests(routes, requests, meta, options, advisory = null) {
|
|
52
52
|
const lines = [];
|
|
53
53
|
const showServer = !options.clientOnly;
|
|
54
54
|
const showClient = !options.serverOnly;
|
|
@@ -109,6 +109,8 @@ function formatRoutesAndRequests(routes, requests, meta, options) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
const routeAdvisory = advisoryLine(advisory);
|
|
113
|
+
if (routeAdvisory) lines.push('', routeAdvisory);
|
|
112
114
|
return lines.join('\n').trimEnd();
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -220,6 +222,7 @@ function formatEndpointsJson(result, options = {}) {
|
|
|
220
222
|
meta: {
|
|
221
223
|
ok: true,
|
|
222
224
|
...meta,
|
|
225
|
+
...(result.advisory && { advisory: result.advisory }),
|
|
223
226
|
// HIGH-2: signal to consumers that bridges array was suppressed
|
|
224
227
|
// because the user filtered to unmatched-only.
|
|
225
228
|
...(unmatchedOnly && { filterMode: 'unmatched' }),
|
|
@@ -17,11 +17,11 @@ function formatFn(match, fnCode) {
|
|
|
17
17
|
lines.push(`${match.relativePath}:${match.startLine}`);
|
|
18
18
|
// Class attribution: three same-name `clear` methods under --all were
|
|
19
19
|
// indistinguishable without their owning class (fix #248).
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
lines.push(`${lineRange(match.startLine, match.endLine)} ${
|
|
20
|
+
const signatureTarget = match.className
|
|
21
|
+
? { ...match, name: `${match.className}.${match.name}` }
|
|
22
|
+
: match;
|
|
23
|
+
const sig = formatFunctionSignature(signatureTarget);
|
|
24
|
+
lines.push(`${lineRange(match.startLine, match.endLine)} ${sig}`);
|
|
25
25
|
lines.push('─'.repeat(60));
|
|
26
26
|
lines.push(fnCode);
|
|
27
27
|
return lines.join('\n');
|
|
@@ -69,10 +69,14 @@ function formatFunctionJson(fn, code) {
|
|
|
69
69
|
* Notes are NOT included — surfaces render those separately (e.g. stderr for CLI).
|
|
70
70
|
* @param {{ entries: Array<{match, code}>, notes: string[] }} result
|
|
71
71
|
*/
|
|
72
|
-
function formatFnResult(result) {
|
|
72
|
+
function formatFnResult(result, options = {}) {
|
|
73
73
|
const parts = [];
|
|
74
|
-
for (const { match, code } of result.entries) {
|
|
75
|
-
|
|
74
|
+
for (const { match, code, truncated, shownLines, totalLines } of result.entries) {
|
|
75
|
+
let text = formatFn(match, code);
|
|
76
|
+
if (truncated) {
|
|
77
|
+
text += `\n... showing ${shownLines} of ${totalLines} lines (${options.maxLinesHint || 'use --max-lines=N or omit it for the full function'})`;
|
|
78
|
+
}
|
|
79
|
+
parts.push(text);
|
|
76
80
|
}
|
|
77
81
|
const separator = result.entries.length > 1 ? '\n\n' + '═'.repeat(60) + '\n\n' : '';
|
|
78
82
|
return parts.join(separator);
|
|
@@ -83,9 +87,16 @@ function formatFnResult(result) {
|
|
|
83
87
|
*/
|
|
84
88
|
function formatFnResultJson(result) {
|
|
85
89
|
if (result.entries.length === 1) {
|
|
86
|
-
|
|
90
|
+
const entry = result.entries[0];
|
|
91
|
+
const value = JSON.parse(formatFunctionJson(entry.match, entry.code));
|
|
92
|
+
if (entry.truncated) {
|
|
93
|
+
value.truncated = true;
|
|
94
|
+
value.shownLines = entry.shownLines;
|
|
95
|
+
value.totalLines = entry.totalLines;
|
|
96
|
+
}
|
|
97
|
+
return JSON.stringify(value, null, 2);
|
|
87
98
|
}
|
|
88
|
-
const arr = result.entries.map(({ match, code }) => ({
|
|
99
|
+
const arr = result.entries.map(({ match, code, truncated, shownLines, totalLines }) => ({
|
|
89
100
|
name: match.name,
|
|
90
101
|
params: match.params,
|
|
91
102
|
paramsStructured: match.paramsStructured || [],
|
|
@@ -101,6 +112,7 @@ function formatFnResultJson(result) {
|
|
|
101
112
|
...(match.isGenerator && { isGenerator: true }),
|
|
102
113
|
file: match.relativePath || match.file,
|
|
103
114
|
code,
|
|
115
|
+
...(truncated && { truncated: true, shownLines, totalLines }),
|
|
104
116
|
}));
|
|
105
117
|
return JSON.stringify(arr, null, 2);
|
|
106
118
|
}
|
|
@@ -109,7 +121,7 @@ function formatFnResultJson(result) {
|
|
|
109
121
|
* Format class handler result (from execute.js).
|
|
110
122
|
* @param {{ entries: Array<{match, code, methods?, summaryMode, truncated, totalLines, maxLines?}>, notes: string[] }} result
|
|
111
123
|
*/
|
|
112
|
-
function formatClassResult(result) {
|
|
124
|
+
function formatClassResult(result, options = {}) {
|
|
113
125
|
const parts = [];
|
|
114
126
|
for (const entry of result.entries) {
|
|
115
127
|
if (entry.summaryMode) {
|
|
@@ -124,7 +136,7 @@ function formatClassResult(result) {
|
|
|
124
136
|
lines.push(` ${formatFunctionSignature(m)} [line ${m.startLine}]`);
|
|
125
137
|
}
|
|
126
138
|
}
|
|
127
|
-
lines.push(`\nClass is ${entry.totalLines} lines. Use --max-lines=N to see source, or "
|
|
139
|
+
lines.push(`\nClass is ${entry.totalLines} lines. ${options.classSourceHint || 'Use --max-lines=N to see source, or "source <method-handle>" for an individual method.'}`);
|
|
128
140
|
parts.push(lines.join('\n'));
|
|
129
141
|
} else if (entry.truncated) {
|
|
130
142
|
parts.push(formatClass(entry.match, entry.code) + `\n\n... showing ${entry.maxLines} of ${entry.totalLines} lines`);
|