ucn 3.8.23 → 3.8.25
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 +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -10
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/core/output/analysis.js
CHANGED
|
@@ -4,7 +4,44 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { langTraits } = require('../../languages');
|
|
7
|
-
const { dynamicImportsNote } = require('./shared');
|
|
7
|
+
const { dynamicImportsNote, formatGitLine } = require('./shared');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One short sentence (~80 chars) of a docstring, suitable for inline display
|
|
11
|
+
* in caller/callee listings.
|
|
12
|
+
*/
|
|
13
|
+
function calleeDocstringSnippet(text) {
|
|
14
|
+
if (!text) return null;
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
const m = trimmed.match(/^(.+?[.!?])(?:\s|$)/);
|
|
17
|
+
let s = m ? m[1] : trimmed;
|
|
18
|
+
if (s.length > 80) s = s.slice(0, 77) + '...';
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a single-line confidence histogram for caller/callee sections.
|
|
24
|
+
* Returns null when there are <= 1 edges (not informative).
|
|
25
|
+
*
|
|
26
|
+
* @param {{high:number, medium:number, low:number, total:number}|null} h
|
|
27
|
+
* @returns {string|null}
|
|
28
|
+
*/
|
|
29
|
+
function formatHistogramLine(h) {
|
|
30
|
+
if (!h || h.total <= 1) return null;
|
|
31
|
+
return ` confidence: ${h.high} high (>0.8), ${h.medium} medium (0.5-0.8), ${h.low} low (<0.5)`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether the formatter should print reachability markers per item.
|
|
36
|
+
* To reduce noise, markers only appear when at least one item is unreachable.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array} items - Caller or callee objects with `reachable` field
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function shouldShowReachability(items) {
|
|
42
|
+
if (!items || items.length === 0) return false;
|
|
43
|
+
return items.some(c => c.reachable === false);
|
|
44
|
+
}
|
|
8
45
|
|
|
9
46
|
/** Format context (callers + callees) as JSON */
|
|
10
47
|
function formatContextJson(context) {
|
|
@@ -52,12 +89,15 @@ function formatContextJson(context) {
|
|
|
52
89
|
file: context.file,
|
|
53
90
|
callerCount: callers.length,
|
|
54
91
|
calleeCount: callees.length,
|
|
92
|
+
callerHistogram: context.callerHistogram || null,
|
|
93
|
+
calleeHistogram: context.calleeHistogram || null,
|
|
55
94
|
callers: callers.map(c => ({
|
|
56
95
|
file: c.relativePath || c.file,
|
|
57
96
|
line: c.line,
|
|
58
97
|
expression: c.content, // FULL expression
|
|
59
98
|
callerName: c.callerName,
|
|
60
99
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
100
|
+
...(c.reachable !== undefined && { reachable: c.reachable }),
|
|
61
101
|
})),
|
|
62
102
|
callees: callees.map(c => ({
|
|
63
103
|
name: c.name,
|
|
@@ -67,6 +107,7 @@ function formatContextJson(context) {
|
|
|
67
107
|
params: c.params, // FULL params
|
|
68
108
|
weight: c.weight || 'normal', // Dependency weight: core, setup, utility
|
|
69
109
|
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
110
|
+
...(c.reachable !== undefined && { reachable: c.reachable }),
|
|
70
111
|
})),
|
|
71
112
|
...(context.warnings && { warnings: context.warnings })
|
|
72
113
|
}
|
|
@@ -143,8 +184,13 @@ function formatContext(ctx, options = {}) {
|
|
|
143
184
|
}
|
|
144
185
|
|
|
145
186
|
// Standard function/method context
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
const compact = !!options.compact;
|
|
188
|
+
if (compact) {
|
|
189
|
+
lines.push(`Context: ${ctx.function}`);
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(`Context for ${ctx.function}:`);
|
|
192
|
+
lines.push('═'.repeat(60));
|
|
193
|
+
}
|
|
148
194
|
|
|
149
195
|
if (ctx.meta) {
|
|
150
196
|
const notes = [];
|
|
@@ -169,14 +215,26 @@ function formatContext(ctx, options = {}) {
|
|
|
169
215
|
|
|
170
216
|
const showConf = options.showConfidence || false;
|
|
171
217
|
const callers = ctx.callers || [];
|
|
172
|
-
lines.push(
|
|
218
|
+
lines.push(`${compact ? '' : '\n'}CALLERS (${callers.length}):`);
|
|
219
|
+
const callerHistLine = showConf ? formatHistogramLine(ctx.callerHistogram) : null;
|
|
220
|
+
if (callerHistLine) lines.push(callerHistLine);
|
|
221
|
+
const showCallerReach = shouldShowReachability(callers);
|
|
173
222
|
for (const c of callers) {
|
|
174
223
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
224
|
+
if (compact) {
|
|
225
|
+
// One line per caller: "[N] file:line [callerName]: expression"
|
|
226
|
+
const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
227
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}: ${expr}`);
|
|
228
|
+
} else {
|
|
229
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
230
|
+
lines.push(` ${c.content.trim()}`);
|
|
231
|
+
}
|
|
232
|
+
if (showConf && c.confidence != null && !compact) {
|
|
178
233
|
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
179
234
|
}
|
|
235
|
+
if (showCallerReach && c.reachable === false && !compact) {
|
|
236
|
+
lines.push(' (unreachable from any entry point)');
|
|
237
|
+
}
|
|
180
238
|
expandable.push({
|
|
181
239
|
num: itemNum++,
|
|
182
240
|
type: 'caller',
|
|
@@ -196,13 +254,31 @@ function formatContext(ctx, options = {}) {
|
|
|
196
254
|
}
|
|
197
255
|
|
|
198
256
|
const callees = ctx.callees || [];
|
|
199
|
-
lines.push(
|
|
257
|
+
lines.push(`${compact ? '' : '\n'}CALLEES (${callees.length}):`);
|
|
258
|
+
const calleeHistLine = showConf ? formatHistogramLine(ctx.calleeHistogram) : null;
|
|
259
|
+
if (calleeHistLine && !compact) lines.push(calleeHistLine);
|
|
260
|
+
const showCalleeReach = shouldShowReachability(callees);
|
|
200
261
|
for (const c of callees) {
|
|
201
262
|
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
202
|
-
|
|
203
|
-
|
|
263
|
+
const returnSuffix = c.returnType ? ` → ${c.returnType}` : '';
|
|
264
|
+
const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
|
|
265
|
+
if (compact) {
|
|
266
|
+
const snip = c.docstring ? calleeDocstringSnippet(c.docstring) : '';
|
|
267
|
+
const docPart = snip ? `: ${snip}` : '';
|
|
268
|
+
lines.push(` [${itemNum}] ${c.name}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}${docPart}`);
|
|
269
|
+
} else {
|
|
270
|
+
lines.push(` [${itemNum}] ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.relativePath}:${c.startLine}`);
|
|
271
|
+
if (c.docstring) {
|
|
272
|
+
const snip = calleeDocstringSnippet(c.docstring);
|
|
273
|
+
if (snip) lines.push(` "${snip}"`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (showConf && c.confidence != null && !compact) {
|
|
204
277
|
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
205
278
|
}
|
|
279
|
+
if (showCalleeReach && c.reachable === false && !compact) {
|
|
280
|
+
lines.push(' (unreachable from any entry point)');
|
|
281
|
+
}
|
|
206
282
|
expandable.push({
|
|
207
283
|
num: itemNum++,
|
|
208
284
|
type: 'callee',
|
|
@@ -227,14 +303,15 @@ function formatImpact(impact, options = {}) {
|
|
|
227
303
|
return 'Function not found.';
|
|
228
304
|
}
|
|
229
305
|
|
|
306
|
+
const compact = !!options.compact;
|
|
230
307
|
const lines = [];
|
|
231
308
|
|
|
232
309
|
// Header
|
|
233
310
|
lines.push(`Impact analysis for ${impact.function}`);
|
|
234
|
-
lines.push('═'.repeat(60));
|
|
311
|
+
if (!compact) lines.push('═'.repeat(60));
|
|
235
312
|
lines.push(`${impact.file}:${impact.startLine}`);
|
|
236
|
-
lines.push(impact.signature);
|
|
237
|
-
lines.push('');
|
|
313
|
+
if (!compact) lines.push(impact.signature);
|
|
314
|
+
if (!compact) lines.push('');
|
|
238
315
|
|
|
239
316
|
// Summary
|
|
240
317
|
if (impact.shownCallSites !== undefined && impact.shownCallSites < impact.totalCallSites) {
|
|
@@ -245,14 +322,21 @@ function formatImpact(impact, options = {}) {
|
|
|
245
322
|
lines.push(` Files affected: ${impact.byFile.length}`);
|
|
246
323
|
|
|
247
324
|
// Patterns
|
|
325
|
+
// BUG-1: also surface structural classification counts (inLoop / inTry /
|
|
326
|
+
// inCallback / inTestCase) — they're already in the JSON shape and in
|
|
327
|
+
// verify text, but were dropped from impact text.
|
|
248
328
|
const p = impact.patterns;
|
|
249
|
-
if (p) {
|
|
329
|
+
if (p && !compact) {
|
|
250
330
|
const patternParts = [];
|
|
251
331
|
if (p.constantArgs > 0) patternParts.push(`${p.constantArgs} with literals`);
|
|
252
332
|
if (p.variableArgs > 0) patternParts.push(`${p.variableArgs} with variables`);
|
|
253
333
|
if (p.awaitedCalls > 0) patternParts.push(`${p.awaitedCalls} awaited`);
|
|
254
334
|
if (p.chainedCalls > 0) patternParts.push(`${p.chainedCalls} chained`);
|
|
255
335
|
if (p.spreadCalls > 0) patternParts.push(`${p.spreadCalls} with spread`);
|
|
336
|
+
if (p.inLoop > 0) patternParts.push(`${p.inLoop} in loop`);
|
|
337
|
+
if (p.inTry > 0) patternParts.push(`${p.inTry} in try`);
|
|
338
|
+
if (p.inCallback > 0) patternParts.push(`${p.inCallback} in callback`);
|
|
339
|
+
if (p.inTestCase > 0) patternParts.push(`${p.inTestCase} in test`);
|
|
256
340
|
if (patternParts.length > 0) {
|
|
257
341
|
lines.push(` Patterns: ${patternParts.join(', ')}`);
|
|
258
342
|
}
|
|
@@ -264,16 +348,38 @@ function formatImpact(impact, options = {}) {
|
|
|
264
348
|
}
|
|
265
349
|
|
|
266
350
|
// By file
|
|
267
|
-
lines.push('');
|
|
351
|
+
if (!compact) lines.push('');
|
|
268
352
|
lines.push('BY FILE:');
|
|
353
|
+
|
|
354
|
+
// Histogram (over the trust signals collected before truncation)
|
|
355
|
+
const impactHistLine = formatHistogramLine(impact.callerHistogram);
|
|
356
|
+
if (impactHistLine) lines.push(impactHistLine);
|
|
357
|
+
|
|
358
|
+
// Compute reachability marker visibility across ALL sites (not per-file)
|
|
359
|
+
const allSites = impact.byFile.flatMap(g => g.sites);
|
|
360
|
+
const showImpactReach = shouldShowReachability(allSites);
|
|
361
|
+
|
|
269
362
|
for (const fileGroup of impact.byFile) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
lines.push(`
|
|
363
|
+
if (compact) {
|
|
364
|
+
// One line per call site, prefixed with file: "file (N) line [caller]: expr"
|
|
365
|
+
for (const site of fileGroup.sites) {
|
|
366
|
+
const caller = site.callerName ? ` [${site.callerName}]` : '';
|
|
367
|
+
const expr = site.expression ? site.expression.replace(/\s+/g, ' ').slice(0, 100) : '';
|
|
368
|
+
const reach = (showImpactReach && site.reachable === false) ? ' (unreachable)' : '';
|
|
369
|
+
lines.push(` ${fileGroup.file}:${site.line}${caller}${reach}: ${expr}`);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
lines.push(`\n${fileGroup.file} (${fileGroup.count} calls)`);
|
|
373
|
+
for (const site of fileGroup.sites) {
|
|
374
|
+
const caller = site.callerName ? `[${site.callerName}]` : '';
|
|
375
|
+
lines.push(` :${site.line} ${caller}`);
|
|
376
|
+
lines.push(` ${site.expression}`);
|
|
377
|
+
if (site.args && site.args.length > 0) {
|
|
378
|
+
lines.push(` args: ${site.args.join(', ')}`);
|
|
379
|
+
}
|
|
380
|
+
if (showImpactReach && site.reachable === false) {
|
|
381
|
+
lines.push(' (unreachable from any entry point)');
|
|
382
|
+
}
|
|
277
383
|
}
|
|
278
384
|
}
|
|
279
385
|
}
|
|
@@ -325,10 +431,12 @@ function formatAbout(about, options = {}) {
|
|
|
325
431
|
return lines.join('\n');
|
|
326
432
|
}
|
|
327
433
|
|
|
434
|
+
const compact = !!options.compact;
|
|
435
|
+
|
|
328
436
|
// Header with signature
|
|
329
437
|
lines.push(`${sym.name} (${sym.type})`);
|
|
330
|
-
lines.push('═'.repeat(60));
|
|
331
|
-
lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}`);
|
|
438
|
+
if (!compact) lines.push('═'.repeat(60));
|
|
439
|
+
lines.push(`${sym.file}:${sym.startLine}-${sym.endLine}${sym.handle ? ' → ' + sym.handle : ''}`);
|
|
332
440
|
if (sym.signature) {
|
|
333
441
|
lines.push(sym.signature);
|
|
334
442
|
}
|
|
@@ -336,6 +444,12 @@ function formatAbout(about, options = {}) {
|
|
|
336
444
|
lines.push(`"${sym.docstring}"`);
|
|
337
445
|
}
|
|
338
446
|
|
|
447
|
+
// Git enrichment (opt-in via --git). Only render when available — non-git
|
|
448
|
+
// dirs and untracked files are skipped silently.
|
|
449
|
+
if (about.git && about.git.available) {
|
|
450
|
+
lines.push(formatGitLine(about.git));
|
|
451
|
+
}
|
|
452
|
+
|
|
339
453
|
// Warnings (show early for visibility)
|
|
340
454
|
if (about.warnings && about.warnings.length > 0) {
|
|
341
455
|
for (const w of about.warnings) {
|
|
@@ -362,6 +476,9 @@ function formatAbout(about, options = {}) {
|
|
|
362
476
|
} else {
|
|
363
477
|
lines.push(`CALLERS (${about.callers.total}):`);
|
|
364
478
|
}
|
|
479
|
+
const aboutCallerHist = showConf ? formatHistogramLine(about.callers.histogram) : null;
|
|
480
|
+
if (aboutCallerHist) lines.push(aboutCallerHist);
|
|
481
|
+
const showAboutCallerReach = shouldShowReachability(about.callers.top);
|
|
365
482
|
for (const c of about.callers.top) {
|
|
366
483
|
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
367
484
|
lines.push(` ${c.file}:${c.line} ${caller}`);
|
|
@@ -369,6 +486,9 @@ function formatAbout(about, options = {}) {
|
|
|
369
486
|
if (showConf && c.confidence != null) {
|
|
370
487
|
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
371
488
|
}
|
|
489
|
+
if (showAboutCallerReach && c.reachable === false) {
|
|
490
|
+
lines.push(' (unreachable from any entry point)');
|
|
491
|
+
}
|
|
372
492
|
}
|
|
373
493
|
}
|
|
374
494
|
|
|
@@ -381,12 +501,24 @@ function formatAbout(about, options = {}) {
|
|
|
381
501
|
} else {
|
|
382
502
|
lines.push(`CALLEES (${about.callees.total}):`);
|
|
383
503
|
}
|
|
504
|
+
const aboutCalleeHist = showConf ? formatHistogramLine(about.callees.histogram) : null;
|
|
505
|
+
if (aboutCalleeHist) lines.push(aboutCalleeHist);
|
|
506
|
+
const showAboutCalleeReach = shouldShowReachability(about.callees.top);
|
|
384
507
|
for (const c of about.callees.top) {
|
|
385
508
|
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
386
|
-
|
|
509
|
+
const returnSuffix = c.returnType ? ` → ${c.returnType}` : '';
|
|
510
|
+
const sideEffects = (c.sideEffects && c.sideEffects.length) ? ` {${c.sideEffects.join(',')}}` : '';
|
|
511
|
+
lines.push(` ${c.name}${weight}${returnSuffix}${sideEffects} - ${c.file}:${c.line} (${c.callCount}x)`);
|
|
512
|
+
if (c.docstring) {
|
|
513
|
+
const snip = calleeDocstringSnippet(c.docstring);
|
|
514
|
+
if (snip) lines.push(` "${snip}"`);
|
|
515
|
+
}
|
|
387
516
|
if (showConf && c.confidence != null) {
|
|
388
517
|
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
389
518
|
}
|
|
519
|
+
if (showAboutCalleeReach && c.reachable === false) {
|
|
520
|
+
lines.push(' (unreachable from any entry point)');
|
|
521
|
+
}
|
|
390
522
|
|
|
391
523
|
// Inline expansion: show first 3 lines of callee code
|
|
392
524
|
if (expand && root && c.file && c.startLine) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/brief.js — Formatter for `brief`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { renderTypedParams, formatGitLine } = require('./shared');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a typed signature line for a callable symbol.
|
|
11
|
+
* Falls back to the raw `params` string when no type info is available.
|
|
12
|
+
*/
|
|
13
|
+
function signatureLine(sym) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (sym.modifiers && sym.modifiers.length) parts.push(sym.modifiers.join(' '));
|
|
16
|
+
let sig = sym.name;
|
|
17
|
+
const typed = renderTypedParams(sym);
|
|
18
|
+
// If we have a structured-params array of length 0, the function has no params.
|
|
19
|
+
// Render `()` rather than the legacy `(...)` placeholder.
|
|
20
|
+
const noParams = Array.isArray(sym.paramsStructured) && sym.paramsStructured.length === 0;
|
|
21
|
+
let paramText;
|
|
22
|
+
if (typed != null) paramText = typed;
|
|
23
|
+
else if (noParams) paramText = '';
|
|
24
|
+
else if (sym.params != null && sym.params !== '...') paramText = sym.params;
|
|
25
|
+
else paramText = '...';
|
|
26
|
+
sig += `(${paramText})`;
|
|
27
|
+
if (sym.returnType) sig += `: ${sym.returnType}`;
|
|
28
|
+
parts.push(sig);
|
|
29
|
+
return parts.join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format a brief result for human/agent consumption.
|
|
34
|
+
*
|
|
35
|
+
* Example output:
|
|
36
|
+
* resolveSymbol(name: string, options: object = {}): {def, definitions, warnings}
|
|
37
|
+
* core/project.js:808-936 (129 lines)
|
|
38
|
+
* "Resolve a symbol name to the best matching definition."
|
|
39
|
+
* async: no, side_effects: [fs], complexity: branches=12, depth=3
|
|
40
|
+
*/
|
|
41
|
+
function formatBrief(result) {
|
|
42
|
+
if (!result) return 'Symbol not found.';
|
|
43
|
+
const lines = [];
|
|
44
|
+
const sym = result.symbol || {};
|
|
45
|
+
|
|
46
|
+
if (result.kind === 'type') {
|
|
47
|
+
lines.push(`${sym.type} ${sym.name}`);
|
|
48
|
+
const lineLabel = `${result.lineCount} line${result.lineCount === 1 ? '' : 's'}`;
|
|
49
|
+
const memberPart = result.memberCount > 0
|
|
50
|
+
? `, ${result.memberCount} member${result.memberCount === 1 ? '' : 's'}`
|
|
51
|
+
: '';
|
|
52
|
+
lines.push(` ${sym.file}:${sym.startLine}-${sym.endLine} (${lineLabel}${memberPart})`);
|
|
53
|
+
if (sym.handle) lines.push(` handle: ${sym.handle}`);
|
|
54
|
+
if (sym.docstring) lines.push(` "${sym.docstring}"`);
|
|
55
|
+
const gitLineType = formatGitLine(result.git);
|
|
56
|
+
if (gitLineType) lines.push(` ${gitLineType}`);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Header line: signature
|
|
61
|
+
lines.push(signatureLine(sym));
|
|
62
|
+
// Location + line count
|
|
63
|
+
lines.push(` ${sym.file}:${sym.startLine}-${sym.endLine} (${result.lineCount || 0} line${result.lineCount === 1 ? '' : 's'})`);
|
|
64
|
+
if (sym.handle) lines.push(` handle: ${sym.handle}`);
|
|
65
|
+
if (sym.docstring) lines.push(` "${sym.docstring}"`);
|
|
66
|
+
if (sym.className) lines.push(` in class ${sym.className}`);
|
|
67
|
+
const gitLineFn = formatGitLine(result.git);
|
|
68
|
+
if (gitLineFn) lines.push(` ${gitLineFn}`);
|
|
69
|
+
|
|
70
|
+
// Async/generator/decorators
|
|
71
|
+
const flags = [];
|
|
72
|
+
flags.push(`async: ${result.isAsync ? 'yes' : 'no'}`);
|
|
73
|
+
if (result.isGenerator) flags.push('generator: yes');
|
|
74
|
+
if (sym.decorators && sym.decorators.length) flags.push(`decorators: [${sym.decorators.join(', ')}]`);
|
|
75
|
+
|
|
76
|
+
// Side effects
|
|
77
|
+
const se = (result.sideEffects && result.sideEffects.length) ? result.sideEffects : ['none'];
|
|
78
|
+
flags.push(`side_effects: [${se.join(', ')}]`);
|
|
79
|
+
|
|
80
|
+
// Complexity
|
|
81
|
+
const c = result.complexity || {};
|
|
82
|
+
const cParts = [];
|
|
83
|
+
if (c.branches != null) cParts.push(`branches=${c.branches}`);
|
|
84
|
+
if (c.maxDepth != null) cParts.push(`depth=${c.maxDepth}`);
|
|
85
|
+
flags.push(`complexity: ${cParts.join(', ')}`);
|
|
86
|
+
|
|
87
|
+
lines.push(' ' + flags.join(' | '));
|
|
88
|
+
if (result.error) lines.push(` Note: ${result.error}`);
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatBriefJson(result) {
|
|
93
|
+
if (!result) return JSON.stringify({ found: false, error: 'Symbol not found' }, null, 2);
|
|
94
|
+
return JSON.stringify({ found: true, ...result }, null, 2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
formatBrief,
|
|
99
|
+
formatBriefJson,
|
|
100
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/check.js — Pre-commit check formatter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
function formatCheck(result) {
|
|
8
|
+
if (!result) return 'No check result.';
|
|
9
|
+
if (result.empty) {
|
|
10
|
+
return `Pre-commit Check (${result.base}${result.staged ? ', staged' : ''})\n${'═'.repeat(60)}\nNo changes to analyze${result.reason ? ` (${result.reason})` : ''}.`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push(`Pre-commit Check vs ${result.base}${result.staged ? ' (staged)' : ''}`);
|
|
15
|
+
lines.push('═'.repeat(60));
|
|
16
|
+
|
|
17
|
+
// Changed functions section
|
|
18
|
+
const items = result.changed || [];
|
|
19
|
+
if (result.truncated) {
|
|
20
|
+
lines.push(`Changed: ${items.length} of ${result.totalChanged} functions`);
|
|
21
|
+
} else {
|
|
22
|
+
lines.push(`Changed: ${items.length} function${items.length === 1 ? '' : 's'}`);
|
|
23
|
+
}
|
|
24
|
+
if (items.length === 0) {
|
|
25
|
+
lines.push(' (none — only non-function changes)');
|
|
26
|
+
} else {
|
|
27
|
+
for (const it of items) {
|
|
28
|
+
const tags = [];
|
|
29
|
+
if (it.kind && it.kind !== 'changed') tags.push(it.kind.toUpperCase());
|
|
30
|
+
if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
|
|
31
|
+
if (it.orphan) tags.push('ORPHAN');
|
|
32
|
+
const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
|
|
33
|
+
const callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
|
|
34
|
+
lines.push(` ${it.name} (${it.file}:${it.line})${tagStr} ${callers}`);
|
|
35
|
+
if (it.mismatches && it.mismatches.length > 0) {
|
|
36
|
+
for (const m of it.mismatches.slice(0, 3)) {
|
|
37
|
+
const where = m.file ? ` at ${m.file}:${m.line}` : '';
|
|
38
|
+
const reason = m.expected
|
|
39
|
+
? `expected ${m.expected}, got ${m.actual}`
|
|
40
|
+
: (m.reason || 'arity mismatch');
|
|
41
|
+
lines.push(` ↳ ${reason}${where}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Tests
|
|
48
|
+
lines.push('');
|
|
49
|
+
if (result.testFiles && result.testFiles.length > 0) {
|
|
50
|
+
lines.push(`Tests potentially affected: ${result.totalTests || result.testFiles.length} in ${result.testFiles.length} file${result.testFiles.length === 1 ? '' : 's'}`);
|
|
51
|
+
for (const tf of result.testFiles.slice(0, 8)) {
|
|
52
|
+
const cnt = tf.testCount ? ` (${tf.testCount})` : '';
|
|
53
|
+
lines.push(` ${tf.file}${cnt}`);
|
|
54
|
+
}
|
|
55
|
+
if (result.testFiles.length > 8) {
|
|
56
|
+
lines.push(` ... and ${result.testFiles.length - 8} more`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
lines.push('Tests: none detected for changed functions');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Action items
|
|
63
|
+
if (result.actions && result.actions.length > 0) {
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('Action items:');
|
|
66
|
+
for (const a of result.actions) {
|
|
67
|
+
const marker = a.severity === 'warn' ? '⚠' : a.severity === 'error' ? '✖' : '·';
|
|
68
|
+
lines.push(` ${marker} ${a.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatCheckJson(result) {
|
|
76
|
+
return JSON.stringify(result, null, 2);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { formatCheck, formatCheckJson };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/output/doctor.js — Project trust report formatter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
function pad(n, width = 4) {
|
|
8
|
+
return String(n).padStart(width);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatDoctor(result) {
|
|
12
|
+
if (!result) return 'No project to analyze.';
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push(`UCN Trust Report — ${result.root}`);
|
|
15
|
+
lines.push('═'.repeat(60));
|
|
16
|
+
lines.push(`Index: ${result.files.scanned} file${result.files.scanned === 1 ? '' : 's'}, ${result.symbols} symbol${result.symbols === 1 ? '' : 's'}`);
|
|
17
|
+
|
|
18
|
+
if (result.filter) lines.push(`Filter: ${result.filter}`);
|
|
19
|
+
|
|
20
|
+
// Languages
|
|
21
|
+
const langEntries = Object.entries(result.languages || {}).sort((a, b) => b[1].files - a[1].files);
|
|
22
|
+
if (langEntries.length) {
|
|
23
|
+
const totalFiles = langEntries.reduce((s, [, v]) => s + v.files, 0) || 1;
|
|
24
|
+
const langStr = langEntries.map(([name, v]) => `${name} (${Math.round(v.files / totalFiles * 100)}%)`).join(', ');
|
|
25
|
+
lines.push(`Languages: ${langStr}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Cache state
|
|
29
|
+
if (result.cache) {
|
|
30
|
+
const state = result.cache.fresh === true ? 'fresh' : result.cache.fresh === false ? 'stale' : 'unknown';
|
|
31
|
+
const buildHint = result.cache.buildMs ? `, ${result.cache.buildMs}ms build` : '';
|
|
32
|
+
lines.push(`Cache: ${state}${buildHint}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Coverage (if computed)
|
|
36
|
+
if (result.coverage && result.coverage.total > 0) {
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push('Resolution coverage (sampled):');
|
|
39
|
+
const c = result.coverage;
|
|
40
|
+
const total = c.total || 1;
|
|
41
|
+
lines.push(` High confidence (>0.8): ${c.high} (${(c.high / total * 100).toFixed(1)}%)`);
|
|
42
|
+
lines.push(` Medium (0.5-0.8): ${c.medium} (${(c.medium / total * 100).toFixed(1)}%)`);
|
|
43
|
+
lines.push(` Low (<0.5): ${c.low} (${(c.low / total * 100).toFixed(1)}%)`);
|
|
44
|
+
lines.push(` Sampled ${c.sampled} symbols → ${c.total} edges examined`);
|
|
45
|
+
} else if (result.coverage) {
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('Resolution coverage: no edges in sample — likely a small or isolated project.');
|
|
48
|
+
} else {
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('Resolution coverage: not computed (use --deep for sampled analysis)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Blind spots
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('Blind spots:');
|
|
56
|
+
const bs = result.blindSpots || {};
|
|
57
|
+
const bsLines = [
|
|
58
|
+
['Dynamic imports', bs.dynamicImports],
|
|
59
|
+
['Eval/exec calls', bs.evalCalls],
|
|
60
|
+
['Reflection', bs.reflection],
|
|
61
|
+
['Parse failures', bs.parseFailures],
|
|
62
|
+
];
|
|
63
|
+
let anyBlindSpot = false;
|
|
64
|
+
for (const [label, info] of bsLines) {
|
|
65
|
+
if (info && info.count > 0) {
|
|
66
|
+
anyBlindSpot = true;
|
|
67
|
+
const sample = info.files.slice(0, 3).map(f => ` - ${f}`).join('\n');
|
|
68
|
+
const more = info.files.length > 3 ? `\n ... and ${info.files.length - 3} more` : '';
|
|
69
|
+
lines.push(` ${label}: ${info.count} in ${info.files.length} file${info.files.length === 1 ? '' : 's'}`);
|
|
70
|
+
if (sample) lines.push(sample + more);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!anyBlindSpot) lines.push(' (none detected)');
|
|
74
|
+
|
|
75
|
+
// Verdict
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`Trust level: ${result.trust}${result.trustReason ? ' — ' + result.trustReason : ''}`);
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatDoctorJson(result) {
|
|
82
|
+
return JSON.stringify(result, null, 2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { formatDoctor, formatDoctorJson };
|