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/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,6 +238,7 @@ 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,
|
|
196
244
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -206,6 +254,7 @@ function formatContextJson(context) {
|
|
|
206
254
|
unverifiedCallers: (context.unverifiedCallers || []).map(c => ({
|
|
207
255
|
file: c.relativePath || c.file,
|
|
208
256
|
line: c.line,
|
|
257
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
209
258
|
expression: c.content,
|
|
210
259
|
callerName: c.callerName ?? null,
|
|
211
260
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -219,6 +268,10 @@ function formatContextJson(context) {
|
|
|
219
268
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
220
269
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
221
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 }),
|
|
222
275
|
})),
|
|
223
276
|
...(context.warnings && { warnings: context.warnings })
|
|
224
277
|
}
|
|
@@ -242,6 +295,7 @@ function formatContextJson(context) {
|
|
|
242
295
|
callers: callers.map(c => ({
|
|
243
296
|
file: c.relativePath || c.file,
|
|
244
297
|
line: c.line,
|
|
298
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
245
299
|
expression: c.content, // FULL expression
|
|
246
300
|
callerName: c.callerName,
|
|
247
301
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -255,6 +309,7 @@ function formatContextJson(context) {
|
|
|
255
309
|
unverifiedCallers: unverifiedCallers.map(c => ({
|
|
256
310
|
file: c.relativePath || c.file,
|
|
257
311
|
line: c.line,
|
|
312
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
258
313
|
expression: c.content, // FULL expression
|
|
259
314
|
callerName: c.callerName ?? null,
|
|
260
315
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
@@ -267,6 +322,10 @@ function formatContextJson(context) {
|
|
|
267
322
|
...(c.dispatchVia && { dispatchVia: c.dispatchVia }),
|
|
268
323
|
...(c.dispatchCandidates != null && { dispatchCandidates: c.dispatchCandidates }),
|
|
269
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 }),
|
|
270
329
|
})),
|
|
271
330
|
callees: callees.map(c => ({
|
|
272
331
|
name: c.name,
|
|
@@ -299,13 +358,14 @@ function formatContext(ctx, options = {}) {
|
|
|
299
358
|
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
300
359
|
|
|
301
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';
|
|
302
362
|
|
|
303
363
|
const lines = [];
|
|
304
364
|
const expandable = [];
|
|
305
365
|
let itemNum = 1;
|
|
306
366
|
|
|
307
367
|
// Handle struct/interface types
|
|
308
|
-
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)) {
|
|
309
369
|
lines.push(`Context for ${ctx.type} ${ctx.name}:`);
|
|
310
370
|
lines.push('═'.repeat(60));
|
|
311
371
|
|
|
@@ -334,8 +394,20 @@ function formatContext(ctx, options = {}) {
|
|
|
334
394
|
});
|
|
335
395
|
}
|
|
336
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
|
+
|
|
337
406
|
const callers = ctx.callers || [];
|
|
338
|
-
|
|
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}):`);
|
|
339
411
|
for (const c of callers) {
|
|
340
412
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
341
413
|
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
@@ -376,7 +448,7 @@ function formatContext(ctx, options = {}) {
|
|
|
376
448
|
shown++;
|
|
377
449
|
}
|
|
378
450
|
if (typeUnverified.length > shown) {
|
|
379
|
-
lines.push(` (+${typeUnverified.length - shown} more unverified —
|
|
451
|
+
lines.push(` (+${typeUnverified.length - shown} more unverified — ${allHint})`);
|
|
380
452
|
}
|
|
381
453
|
}
|
|
382
454
|
|
|
@@ -424,9 +496,12 @@ function formatContext(ctx, options = {}) {
|
|
|
424
496
|
const callers = ctx.callers || [];
|
|
425
497
|
const prodCallers = callers.filter(c => !isTestEntry(c));
|
|
426
498
|
const testCallers = callers.filter(c => isTestEntry(c));
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
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}):`;
|
|
430
505
|
lines.push(`${compact ? '' : '\n'}${tierHeader}`);
|
|
431
506
|
const callerEvidence = options.showConfidence !== false ? formatEvidenceLine(callers) : null;
|
|
432
507
|
if (callerEvidence) lines.push(callerEvidence);
|
|
@@ -464,15 +539,123 @@ function formatContext(ctx, options = {}) {
|
|
|
464
539
|
// candidates ARE analyzed — they render in the unverified band with
|
|
465
540
|
// reasons, and the ACCOUNT line reconciles every text occurrence.
|
|
466
541
|
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
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.
|
|
470
548
|
const unverified = ctx.unverifiedCallers || [];
|
|
471
|
-
|
|
472
|
-
|
|
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:`);
|
|
473
656
|
const cap = (ctx.meta && ctx.meta.all) ? Infinity : 10;
|
|
474
657
|
let shown = 0;
|
|
475
|
-
for (const u of
|
|
658
|
+
for (const u of actionableUnverified) {
|
|
476
659
|
if (shown >= cap) break;
|
|
477
660
|
const callerName = u.callerName ? ` [${u.callerName}]` : '';
|
|
478
661
|
const reason = u.reason ? ` (${unverifiedReasonLabel(u)})` : '';
|
|
@@ -490,13 +673,16 @@ function formatContext(ctx, options = {}) {
|
|
|
490
673
|
});
|
|
491
674
|
shown++;
|
|
492
675
|
}
|
|
493
|
-
if (
|
|
494
|
-
lines.push(` (+${
|
|
676
|
+
if (actionableUnverified.length > shown) {
|
|
677
|
+
lines.push(` (+${actionableUnverified.length - shown} more unverified — ${allHint})`);
|
|
495
678
|
}
|
|
496
679
|
}
|
|
497
680
|
|
|
498
681
|
const callees = ctx.callees || [];
|
|
499
|
-
|
|
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}):`);
|
|
500
686
|
const calleeEvidence = options.showConfidence !== false ? formatEvidenceLine(callees) : null;
|
|
501
687
|
if (calleeEvidence) lines.push(calleeEvidence);
|
|
502
688
|
const calleeReach = reachabilityDisplay(callees, hasEntrypoints, 'callee');
|
|
@@ -535,7 +721,7 @@ function formatContext(ctx, options = {}) {
|
|
|
535
721
|
// Conservation contract lines: non-call summary + ACCOUNT/WARNING/FILTERED
|
|
536
722
|
const account = ctx.meta && ctx.meta.account;
|
|
537
723
|
if (account) {
|
|
538
|
-
const nonCallLine = formatNonCallLine(account, ctx.function);
|
|
724
|
+
const nonCallLine = formatNonCallLine(account, ctx.function, options.usagesHint);
|
|
539
725
|
if (nonCallLine) lines.push(`${compact ? '' : '\n'}${nonCallLine}`);
|
|
540
726
|
const accountLines = formatAccountLines(account);
|
|
541
727
|
if (accountLines.length > 0) {
|
|
@@ -567,6 +753,9 @@ function formatImpact(impact, options = {}) {
|
|
|
567
753
|
if (!compact) lines.push('═'.repeat(60));
|
|
568
754
|
lines.push(`${impact.file}:${impact.startLine}`);
|
|
569
755
|
if (!compact) lines.push(impact.signature);
|
|
756
|
+
for (const warning of impact.warnings || []) {
|
|
757
|
+
lines.push(`Note: ${warning.message}`);
|
|
758
|
+
}
|
|
570
759
|
if (!compact) lines.push('');
|
|
571
760
|
|
|
572
761
|
// Summary (confirmed + unverified tiers reported separately)
|
|
@@ -693,6 +882,7 @@ function formatAbout(about, options = {}) {
|
|
|
693
882
|
}
|
|
694
883
|
|
|
695
884
|
const lines = [];
|
|
885
|
+
const allHint = options.allHint || 'use --all';
|
|
696
886
|
const sym = about.symbol;
|
|
697
887
|
const { expand, root, depth } = options;
|
|
698
888
|
|
|
@@ -809,7 +999,7 @@ function formatAbout(about, options = {}) {
|
|
|
809
999
|
lines.push(` ${u.file}:${u.line}${caller}${expr}${reason}`);
|
|
810
1000
|
}
|
|
811
1001
|
if (aboutUnverified.total > aboutUnverified.top.length) {
|
|
812
|
-
lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified —
|
|
1002
|
+
lines.push(` (+${aboutUnverified.total - aboutUnverified.top.length} more unverified — ${allHint})`);
|
|
813
1003
|
}
|
|
814
1004
|
}
|
|
815
1005
|
|
|
@@ -927,7 +1117,7 @@ function formatAbout(about, options = {}) {
|
|
|
927
1117
|
lines.push(about.code);
|
|
928
1118
|
} else if (about.code && compact) {
|
|
929
1119
|
lines.push('');
|
|
930
|
-
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.`);
|
|
931
1121
|
}
|
|
932
1122
|
|
|
933
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`);
|