projscan 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/dist/cli/commands/doctor.js +18 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/explain.js +1 -0
- package/dist/cli/commands/explain.js.map +1 -1
- package/dist/cli/commands/memory.d.ts +11 -0
- package/dist/cli/commands/memory.js +175 -0
- package/dist/cli/commands/memory.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/codeGraph.js +190 -241
- package/dist/core/codeGraph.js.map +1 -1
- package/dist/core/fileInspector.js +40 -44
- package/dist/core/fileInspector.js.map +1 -1
- package/dist/core/hotspotAnalyzer.js +65 -19
- package/dist/core/hotspotAnalyzer.js.map +1 -1
- package/dist/core/issueEngine.js +24 -0
- package/dist/core/issueEngine.js.map +1 -1
- package/dist/core/languages/csharpImports.js +6 -4
- package/dist/core/languages/csharpImports.js.map +1 -1
- package/dist/core/memory.d.ts +154 -0
- package/dist/core/memory.js +277 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/review.d.ts +25 -1
- package/dist/core/review.js +84 -0
- package/dist/core/review.js.map +1 -1
- package/dist/mcp/prompts.js +272 -0
- package/dist/mcp/prompts.js.map +1 -1
- package/dist/mcp/server.js +162 -146
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tokenBudget.d.ts +22 -0
- package/dist/mcp/tokenBudget.js +26 -0
- package/dist/mcp/tokenBudget.js.map +1 -1
- package/dist/mcp/tools/doctor.js +65 -2
- package/dist/mcp/tools/doctor.js.map +1 -1
- package/dist/mcp/tools/explain.js +4 -3
- package/dist/mcp/tools/explain.js.map +1 -1
- package/dist/mcp/tools/explainIssue.js +3 -2
- package/dist/mcp/tools/explainIssue.js.map +1 -1
- package/dist/mcp/tools/file.js +3 -2
- package/dist/mcp/tools/file.js.map +1 -1
- package/dist/mcp/tools/graph.js +16 -11
- package/dist/mcp/tools/graph.js.map +1 -1
- package/dist/mcp/tools/impact.js +2 -2
- package/dist/mcp/tools/impact.js.map +1 -1
- package/dist/mcp/tools/memory.d.ts +19 -0
- package/dist/mcp/tools/memory.js +134 -0
- package/dist/mcp/tools/memory.js.map +1 -0
- package/dist/mcp/tools/review.js +25 -4
- package/dist/mcp/tools/review.js.map +1 -1
- package/dist/mcp/tools/upgrade.js +3 -2
- package/dist/mcp/tools/upgrade.js.map +1 -1
- package/dist/mcp/tools.js +2 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reporters/consoleReporter.d.ts +12 -1
- package/dist/reporters/consoleReporter.js +289 -179
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.js +185 -128
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/tool-manifest.json +43 -6
- package/dist/types.d.ts +21 -0
- package/dist/utils/config.js +76 -51
- package/dist/utils/config.js.map +1 -1
- package/package.json +8 -1
|
@@ -79,11 +79,21 @@ export function reportAnalysis(report) {
|
|
|
79
79
|
}
|
|
80
80
|
console.log('');
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
export function reportHealth(issues, scanTimeMsOrOptions) {
|
|
83
|
+
const opts = typeof scanTimeMsOrOptions === 'number'
|
|
84
|
+
? { scanTimeMs: scanTimeMsOrOptions }
|
|
85
|
+
: (scanTimeMsOrOptions ?? {});
|
|
84
86
|
console.log(header('Project Health Report'));
|
|
85
87
|
const { score, grade } = calculateScore(issues);
|
|
86
|
-
const gradeColor = grade === 'A'
|
|
88
|
+
const gradeColor = grade === 'A'
|
|
89
|
+
? chalk.green
|
|
90
|
+
: grade === 'B'
|
|
91
|
+
? chalk.green
|
|
92
|
+
: grade === 'C'
|
|
93
|
+
? chalk.yellow
|
|
94
|
+
: grade === 'D'
|
|
95
|
+
? chalk.yellow
|
|
96
|
+
: chalk.red;
|
|
87
97
|
console.log(`\n Health Score: ${gradeColor(chalk.bold(`${grade} (${score}/100)`))}`);
|
|
88
98
|
if (issues.length === 0) {
|
|
89
99
|
console.log(` ${chalk.green('✓')} ${chalk.bold('No issues detected!')} Your project looks healthy.\n`);
|
|
@@ -101,8 +111,8 @@ export function reportHealth(issues, scanTimeMs) {
|
|
|
101
111
|
if (infos.length > 0)
|
|
102
112
|
parts.push(chalk.blue(`${infos.length} info`));
|
|
103
113
|
console.log(` Found ${parts.join(', ')}`);
|
|
104
|
-
if (scanTimeMs !== undefined) {
|
|
105
|
-
console.log(` Scanned in ${chalk.dim(scanTimeMs.toFixed(0) + 'ms')}`);
|
|
114
|
+
if (opts.scanTimeMs !== undefined) {
|
|
115
|
+
console.log(` Scanned in ${chalk.dim(opts.scanTimeMs.toFixed(0) + 'ms')}`);
|
|
106
116
|
}
|
|
107
117
|
// Issues
|
|
108
118
|
console.log(header('Issues Detected'));
|
|
@@ -122,6 +132,11 @@ export function reportHealth(issues, scanTimeMs) {
|
|
|
122
132
|
}
|
|
123
133
|
console.log(`\n Run ${chalk.bold.cyan('projscan fix')} to auto-fix ${fixable.length} issue${fixable.length > 1 ? 's' : ''}.\n`);
|
|
124
134
|
}
|
|
135
|
+
// 1.5+ — Project Memory tip. Only fires when the user has stable
|
|
136
|
+
// rules accumulating; quiet otherwise.
|
|
137
|
+
if (opts.stableRuleCount && opts.stableRuleCount > 0) {
|
|
138
|
+
console.log(` ${chalk.cyan('▲')} ${chalk.dim(`${opts.stableRuleCount} rule${opts.stableRuleCount === 1 ? ' has' : 's have'} been open across enough runs to count as accepted. Run`)} ${chalk.bold.cyan('projscan memory stable')} ${chalk.dim('to review and silence them in .projscanrc.')}\n`);
|
|
139
|
+
}
|
|
125
140
|
console.log('');
|
|
126
141
|
}
|
|
127
142
|
// ── Report: ci ────────────────────────────────────────────
|
|
@@ -140,58 +155,77 @@ export function reportCi(issues, threshold) {
|
|
|
140
155
|
// ── Report: diff ──────────────────────────────────────────
|
|
141
156
|
export function reportDiff(diff) {
|
|
142
157
|
console.log(header('Health Diff'));
|
|
158
|
+
printDiffScoreLine(diff);
|
|
159
|
+
printDiffIssueLists(diff);
|
|
160
|
+
printHotspotDiff(diff);
|
|
161
|
+
console.log(`\n Baseline: ${chalk.dim(diff.before.timestamp)}`);
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
function printDiffScoreLine(diff) {
|
|
143
165
|
const arrow = diff.scoreDelta > 0 ? chalk.green('↑') : diff.scoreDelta < 0 ? chalk.red('↓') : chalk.dim('-');
|
|
144
166
|
const delta = diff.scoreDelta > 0 ? `+${diff.scoreDelta}` : String(diff.scoreDelta);
|
|
145
167
|
console.log(`\n Score: ${diff.before.score} → ${diff.after.score} (${delta}) ${arrow}`);
|
|
146
168
|
console.log(` Grade: ${diff.before.grade} → ${diff.after.grade}`);
|
|
169
|
+
}
|
|
170
|
+
function printDiffIssueLists(diff) {
|
|
147
171
|
if (diff.resolvedIssues.length > 0) {
|
|
148
172
|
console.log(`\n ${chalk.green('✓')} Resolved (${diff.resolvedIssues.length}):`);
|
|
149
|
-
for (const title of diff.resolvedIssues)
|
|
173
|
+
for (const title of diff.resolvedIssues)
|
|
150
174
|
console.log(` ${chalk.green('-')} ${title}`);
|
|
151
|
-
}
|
|
152
175
|
}
|
|
153
176
|
if (diff.newIssues.length > 0) {
|
|
154
177
|
console.log(`\n ${chalk.red('✗')} New (${diff.newIssues.length}):`);
|
|
155
|
-
for (const title of diff.newIssues)
|
|
178
|
+
for (const title of diff.newIssues)
|
|
156
179
|
console.log(` ${chalk.red('-')} ${title}`);
|
|
157
|
-
}
|
|
158
180
|
}
|
|
159
181
|
if (diff.resolvedIssues.length === 0 && diff.newIssues.length === 0) {
|
|
160
182
|
console.log(`\n ${chalk.dim('No change in issues.')}`);
|
|
161
183
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
}
|
|
185
|
+
function printHotspotDiff(diff) {
|
|
186
|
+
if (!diff.hotspotDiff)
|
|
187
|
+
return;
|
|
188
|
+
const hd = diff.hotspotDiff;
|
|
189
|
+
const total = hd.rose.length + hd.fell.length + hd.appeared.length + hd.resolved.length;
|
|
190
|
+
if (total === 0)
|
|
191
|
+
return;
|
|
192
|
+
console.log(header('Hotspot Changes'));
|
|
193
|
+
printHotspotRose(hd.rose);
|
|
194
|
+
printHotspotAppeared(hd.appeared);
|
|
195
|
+
printHotspotFell(hd.fell);
|
|
196
|
+
printHotspotResolved(hd.resolved);
|
|
197
|
+
}
|
|
198
|
+
function printHotspotRose(rose) {
|
|
199
|
+
if (rose.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
console.log(`\n ${chalk.red('▲')} Worsening (${rose.length}):`);
|
|
202
|
+
for (const delta of rose.slice(0, 10)) {
|
|
203
|
+
console.log(` ${chalk.red('+' + delta.scoreDelta.toFixed(1))} ${delta.relativePath} ${chalk.dim(`${delta.beforeScore?.toFixed(1)} → ${delta.afterScore?.toFixed(1)}`)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function printHotspotAppeared(appeared) {
|
|
207
|
+
if (appeared.length === 0)
|
|
208
|
+
return;
|
|
209
|
+
console.log(`\n ${chalk.yellow('●')} Newly risky (${appeared.length}):`);
|
|
210
|
+
for (const delta of appeared.slice(0, 10)) {
|
|
211
|
+
console.log(` ${chalk.yellow(delta.afterScore?.toFixed(1) ?? '?')} ${delta.relativePath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function printHotspotFell(fell) {
|
|
215
|
+
if (fell.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
console.log(`\n ${chalk.green('▼')} Improving (${fell.length}):`);
|
|
218
|
+
for (const delta of fell.slice(0, 10)) {
|
|
219
|
+
console.log(` ${chalk.green(delta.scoreDelta.toFixed(1))} ${delta.relativePath} ${chalk.dim(`${delta.beforeScore?.toFixed(1)} → ${delta.afterScore?.toFixed(1)}`)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function printHotspotResolved(resolved) {
|
|
223
|
+
if (resolved.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
console.log(`\n ${chalk.green('✓')} No longer tracked (${resolved.length}):`);
|
|
226
|
+
for (const delta of resolved.slice(0, 5)) {
|
|
227
|
+
console.log(` ${chalk.green('-')} ${delta.relativePath}`);
|
|
192
228
|
}
|
|
193
|
-
console.log(`\n Baseline: ${chalk.dim(diff.before.timestamp)}`);
|
|
194
|
-
console.log('');
|
|
195
229
|
}
|
|
196
230
|
// ── Report: fix ───────────────────────────────────────────
|
|
197
231
|
export function reportDetectedIssues(issues, fixes) {
|
|
@@ -327,15 +361,25 @@ export function reportHotspots(report) {
|
|
|
327
361
|
}
|
|
328
362
|
console.log(chalk.dim(`\n ${report.window.commitsScanned} commit${report.window.commitsScanned === 1 ? '' : 's'} since ${report.window.since} · ${report.totalFilesRanked} file${report.totalFilesRanked === 1 ? '' : 's'} ranked\n`));
|
|
329
363
|
const maxScore = report.hotspots[0]?.riskScore ?? 1;
|
|
364
|
+
let hasAccepted = false;
|
|
330
365
|
for (let i = 0; i < report.hotspots.length; i++) {
|
|
331
366
|
const h = report.hotspots[i];
|
|
332
367
|
const rank = chalk.bold(String(i + 1).padStart(2, ' ') + '.');
|
|
333
368
|
const scoreLabel = chalk.bold(h.riskScore.toFixed(1).padStart(5, ' '));
|
|
334
369
|
const barPct = Math.min(100, Math.round((h.riskScore / maxScore) * 100));
|
|
335
|
-
|
|
370
|
+
// 1.5+ — accepted hotspots (Project Memory marked them as
|
|
371
|
+
// implicitly-accepted load-bearing debt) get a dim [accepted] tag
|
|
372
|
+
// so users aren't repeatedly pestered about the same files.
|
|
373
|
+
const acceptedTag = h.accepted ? ` ${chalk.dim('[accepted]')}` : '';
|
|
374
|
+
if (h.accepted)
|
|
375
|
+
hasAccepted = true;
|
|
376
|
+
console.log(` ${rank} ${scoreLabel} ${bar(barPct, 14)} ${chalk.cyan(h.relativePath)}${acceptedTag}`);
|
|
336
377
|
const reasonStr = h.reasons.length > 0 ? h.reasons.join(', ') : 'ranked by risk';
|
|
337
378
|
console.log(` ${chalk.dim(reasonStr)}`);
|
|
338
379
|
}
|
|
380
|
+
if (hasAccepted) {
|
|
381
|
+
console.log(chalk.dim(`\n ${chalk.cyan('▲')} [accepted] = top-5 for ≥ 5 runs over ≥ 7 days without improving (Project Memory).`));
|
|
382
|
+
}
|
|
339
383
|
console.log(chalk.dim(`\n Tip: run ${chalk.bold.cyan('projscan file <file>')} to drill into a hotspot.\n`));
|
|
340
384
|
}
|
|
341
385
|
// ── Report: file (drill-down) ─────────────────────────────
|
|
@@ -345,6 +389,16 @@ export function reportFileInspection(insp) {
|
|
|
345
389
|
console.log(`\n ${chalk.red('✗')} ${insp.reason ?? 'File unavailable.'}\n`);
|
|
346
390
|
return;
|
|
347
391
|
}
|
|
392
|
+
printFileSummary(insp);
|
|
393
|
+
printFileHotspot(insp);
|
|
394
|
+
printFileIssues(insp);
|
|
395
|
+
printFilePotentialIssues(insp);
|
|
396
|
+
printFileImports(insp);
|
|
397
|
+
printFileExports(insp);
|
|
398
|
+
printFileFunctions(insp);
|
|
399
|
+
console.log('');
|
|
400
|
+
}
|
|
401
|
+
function printFileSummary(insp) {
|
|
348
402
|
console.log(`\n ${chalk.bold('File:')} ${insp.relativePath}`);
|
|
349
403
|
console.log(` ${chalk.bold('Purpose:')} ${insp.purpose}`);
|
|
350
404
|
console.log(` ${chalk.bold('Lines:')} ${insp.lineCount}`);
|
|
@@ -355,66 +409,83 @@ export function reportFileInspection(insp) {
|
|
|
355
409
|
if (typeof insp.fanIn === 'number' || typeof insp.fanOut === 'number') {
|
|
356
410
|
console.log(` ${chalk.bold('Coupling:')} fan-in ${insp.fanIn ?? '-'}, fan-out ${insp.fanOut ?? '-'}`);
|
|
357
411
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
console.log(` ${chalk.bold('Risk Score:')} ${chalk.bold(h.riskScore.toFixed(1))}`);
|
|
362
|
-
console.log(` ${chalk.bold('Commits:')} ${h.churn}`);
|
|
363
|
-
console.log(` ${chalk.bold('Authors:')} ${h.distinctAuthors}${h.primaryAuthor ? ` (primary: ${formatAuthorEmail(h.primaryAuthor)}, ${Math.round(h.primaryAuthorShare * 100)}%)` : ''}`);
|
|
364
|
-
if (h.daysSinceLastChange !== null) {
|
|
365
|
-
console.log(` ${chalk.bold('Last change:')} ${h.daysSinceLastChange} days ago`);
|
|
366
|
-
}
|
|
367
|
-
if (h.busFactorOne) {
|
|
368
|
-
console.log(` ${chalk.red('⚠')} Bus factor 1 - only one author has touched this.`);
|
|
369
|
-
}
|
|
370
|
-
if (h.reasons.length > 0) {
|
|
371
|
-
console.log(` ${chalk.dim(h.reasons.join(', '))}`);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
412
|
+
}
|
|
413
|
+
function printFileHotspot(insp) {
|
|
414
|
+
if (!insp.hotspot) {
|
|
375
415
|
console.log(chalk.dim('\n No hotspot data (file is untouched in git window or outside analysis scope).'));
|
|
416
|
+
return;
|
|
376
417
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
418
|
+
const h = insp.hotspot;
|
|
419
|
+
console.log(header('Risk'));
|
|
420
|
+
console.log(` ${chalk.bold('Risk Score:')} ${chalk.bold(h.riskScore.toFixed(1))}`);
|
|
421
|
+
console.log(` ${chalk.bold('Commits:')} ${h.churn}`);
|
|
422
|
+
const primary = h.primaryAuthor
|
|
423
|
+
? ` (primary: ${formatAuthorEmail(h.primaryAuthor)}, ${Math.round(h.primaryAuthorShare * 100)}%)`
|
|
424
|
+
: '';
|
|
425
|
+
console.log(` ${chalk.bold('Authors:')} ${h.distinctAuthors}${primary}`);
|
|
426
|
+
if (h.daysSinceLastChange !== null) {
|
|
427
|
+
console.log(` ${chalk.bold('Last change:')} ${h.daysSinceLastChange} days ago`);
|
|
382
428
|
}
|
|
383
|
-
if (
|
|
384
|
-
console.log(
|
|
385
|
-
for (const issue of insp.potentialIssues) {
|
|
386
|
-
console.log(` ${chalk.yellow('⚠')} ${issue}`);
|
|
387
|
-
}
|
|
429
|
+
if (h.busFactorOne) {
|
|
430
|
+
console.log(` ${chalk.red('⚠')} Bus factor 1 - only one author has touched this.`);
|
|
388
431
|
}
|
|
389
|
-
if (
|
|
390
|
-
console.log(
|
|
391
|
-
for (const imp of insp.imports.slice(0, 20)) {
|
|
392
|
-
const prefix = imp.isRelative ? chalk.dim('(local)') : chalk.cyan('(package)');
|
|
393
|
-
console.log(` ${prefix} ${imp.source}`);
|
|
394
|
-
}
|
|
395
|
-
if (insp.imports.length > 20) {
|
|
396
|
-
console.log(chalk.dim(` ... and ${insp.imports.length - 20} more`));
|
|
397
|
-
}
|
|
432
|
+
if (h.reasons.length > 0) {
|
|
433
|
+
console.log(` ${chalk.dim(h.reasons.join(', '))}`);
|
|
398
434
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
}
|
|
436
|
+
function printFileIssues(insp) {
|
|
437
|
+
if (insp.issues.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
console.log(header('Related Issues'));
|
|
440
|
+
for (const issue of insp.issues) {
|
|
441
|
+
console.log(` ${severityIcon(issue.severity)} ${issue.title}`);
|
|
404
442
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
443
|
+
}
|
|
444
|
+
function printFilePotentialIssues(insp) {
|
|
445
|
+
if (insp.potentialIssues.length === 0)
|
|
446
|
+
return;
|
|
447
|
+
console.log(header('Potential Issues'));
|
|
448
|
+
for (const issue of insp.potentialIssues) {
|
|
449
|
+
console.log(` ${chalk.yellow('⚠')} ${issue}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function printFileImports(insp) {
|
|
453
|
+
if (insp.imports.length === 0)
|
|
454
|
+
return;
|
|
455
|
+
console.log(header('Dependencies'));
|
|
456
|
+
for (const imp of insp.imports.slice(0, 20)) {
|
|
457
|
+
const prefix = imp.isRelative ? chalk.dim('(local)') : chalk.cyan('(package)');
|
|
458
|
+
console.log(` ${prefix} ${imp.source}`);
|
|
459
|
+
}
|
|
460
|
+
if (insp.imports.length > 20) {
|
|
461
|
+
console.log(chalk.dim(` ... and ${insp.imports.length - 20} more`));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function printFileExports(insp) {
|
|
465
|
+
if (insp.exports.length === 0)
|
|
466
|
+
return;
|
|
467
|
+
console.log(header('Exports'));
|
|
468
|
+
for (const exp of insp.exports) {
|
|
469
|
+
console.log(` ${chalk.dim('•')} ${chalk.bold(exp.name)} ${chalk.dim(`(${exp.type})`)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function printFileFunctions(insp) {
|
|
473
|
+
if (!insp.functions || insp.functions.length === 0)
|
|
474
|
+
return;
|
|
475
|
+
console.log(header('Functions (top by CC)'));
|
|
476
|
+
const top = insp.functions.slice(0, 10);
|
|
477
|
+
for (const fn of top) {
|
|
478
|
+
const ccColor = fn.cyclomaticComplexity >= 10
|
|
479
|
+
? chalk.red
|
|
480
|
+
: fn.cyclomaticComplexity >= 5
|
|
481
|
+
? chalk.yellow
|
|
482
|
+
: chalk.dim;
|
|
483
|
+
const fiStr = typeof fn.fanIn === 'number' ? `fan-in ${String(fn.fanIn).padStart(2)}` : ' ';
|
|
484
|
+
console.log(` ${ccColor(`CC ${String(fn.cyclomaticComplexity).padStart(3)}`)} ${chalk.dim(fiStr)} ${chalk.bold(fn.name)} ${chalk.dim(`L${fn.line}-${fn.endLine}`)}`);
|
|
485
|
+
}
|
|
486
|
+
if (insp.functions.length > 10) {
|
|
487
|
+
console.log(chalk.dim(` ... and ${insp.functions.length - 10} more`));
|
|
416
488
|
}
|
|
417
|
-
console.log('');
|
|
418
489
|
}
|
|
419
490
|
function formatSize(bytes) {
|
|
420
491
|
if (bytes < 1024)
|
|
@@ -640,46 +711,64 @@ export function reportPrDiff(report) {
|
|
|
640
711
|
console.log(`\n ${chalk.yellow('⚠')} ${report.reason ?? 'PR diff unavailable.'}\n`);
|
|
641
712
|
return;
|
|
642
713
|
}
|
|
714
|
+
printPrDiffRefs(report);
|
|
715
|
+
printPrDiffFileTotals(report);
|
|
716
|
+
printPrDiffAdded(report);
|
|
717
|
+
printPrDiffRemoved(report);
|
|
718
|
+
printPrDiffModified(report);
|
|
719
|
+
}
|
|
720
|
+
function printPrDiffRefs(report) {
|
|
643
721
|
console.log(chalk.dim(`\n base ${report.base.ref} (${report.base.resolvedSha?.slice(0, 7) ?? '?'}) → head ${report.head.ref} (${report.head.resolvedSha?.slice(0, 7) ?? '?'})\n`));
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
722
|
+
}
|
|
723
|
+
function printPrDiffFileTotals(report) {
|
|
724
|
+
const fileLabel = report.totalFilesChanged === 1 ? '' : 's';
|
|
725
|
+
console.log(` ${chalk.bold(report.totalFilesChanged.toString())} file${fileLabel} changed: ${chalk.green(`+${report.filesAdded.length}`)} ${chalk.red(`-${report.filesRemoved.length}`)} ${chalk.yellow(`~${report.filesModified.length}`)}\n`);
|
|
726
|
+
}
|
|
727
|
+
function printPrDiffAdded(report) {
|
|
728
|
+
if (report.filesAdded.length === 0)
|
|
729
|
+
return;
|
|
730
|
+
console.log(chalk.bold(' Added:'));
|
|
731
|
+
for (const f of report.filesAdded)
|
|
732
|
+
console.log(` ${chalk.green('+')} ${f}`);
|
|
733
|
+
console.log('');
|
|
734
|
+
}
|
|
735
|
+
function printPrDiffRemoved(report) {
|
|
736
|
+
if (report.filesRemoved.length === 0)
|
|
737
|
+
return;
|
|
738
|
+
console.log(chalk.bold(' Removed:'));
|
|
739
|
+
for (const f of report.filesRemoved)
|
|
740
|
+
console.log(` ${chalk.red('-')} ${f}`);
|
|
741
|
+
console.log('');
|
|
742
|
+
}
|
|
743
|
+
function printPrDiffModified(report) {
|
|
744
|
+
if (report.filesModified.length === 0)
|
|
745
|
+
return;
|
|
746
|
+
console.log(chalk.bold(' Modified:'));
|
|
747
|
+
for (const m of report.filesModified)
|
|
748
|
+
printPrDiffModifiedFile(m);
|
|
749
|
+
console.log('');
|
|
750
|
+
}
|
|
751
|
+
function printPrDiffModifiedFile(m) {
|
|
752
|
+
const ccDelta = m.cyclomaticDelta;
|
|
753
|
+
const fiDelta = m.fanInDelta;
|
|
754
|
+
const ccStr = ccDelta === null ? '' : `, ΔCC ${signed(ccDelta)}`;
|
|
755
|
+
const finStr = fiDelta === null || fiDelta === 0 ? '' : `, Δfan-in ${signed(fiDelta)}`;
|
|
756
|
+
console.log(` ${chalk.yellow('~')} ${chalk.cyan(m.relativePath)}${chalk.dim(ccStr + finStr)}`);
|
|
757
|
+
if (m.exportsAdded.length > 0) {
|
|
758
|
+
console.log(` ${chalk.green('+exports:')} ${m.exportsAdded.join(', ')}`);
|
|
650
759
|
}
|
|
651
|
-
if (
|
|
652
|
-
console.log(chalk.
|
|
653
|
-
for (const f of report.filesRemoved)
|
|
654
|
-
console.log(` ${chalk.red('-')} ${f}`);
|
|
655
|
-
console.log('');
|
|
760
|
+
if (m.exportsRemoved.length > 0) {
|
|
761
|
+
console.log(` ${chalk.red('-exports:')} ${m.exportsRemoved.join(', ')}`);
|
|
656
762
|
}
|
|
657
|
-
if (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
console.log(` ${chalk.green('+exports:')} ${m.exportsAdded.join(', ')}`);
|
|
667
|
-
}
|
|
668
|
-
if (m.exportsRemoved.length > 0) {
|
|
669
|
-
console.log(` ${chalk.red('-exports:')} ${m.exportsRemoved.join(', ')}`);
|
|
670
|
-
}
|
|
671
|
-
if (m.exportsRenamed.length > 0) {
|
|
672
|
-
const pairs = m.exportsRenamed.map((r) => `${r.from} → ${r.to}`).join(', ');
|
|
673
|
-
console.log(` ${chalk.yellow('~exports:')} ${pairs}`);
|
|
674
|
-
}
|
|
675
|
-
if (m.importsAdded.length > 0) {
|
|
676
|
-
console.log(` ${chalk.green('+imports:')} ${m.importsAdded.join(', ')}`);
|
|
677
|
-
}
|
|
678
|
-
if (m.importsRemoved.length > 0) {
|
|
679
|
-
console.log(` ${chalk.red('-imports:')} ${m.importsRemoved.join(', ')}`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
console.log('');
|
|
763
|
+
if (m.exportsRenamed.length > 0) {
|
|
764
|
+
const pairs = m.exportsRenamed.map((r) => `${r.from} → ${r.to}`).join(', ');
|
|
765
|
+
console.log(` ${chalk.yellow('~exports:')} ${pairs}`);
|
|
766
|
+
}
|
|
767
|
+
if (m.importsAdded.length > 0) {
|
|
768
|
+
console.log(` ${chalk.green('+imports:')} ${m.importsAdded.join(', ')}`);
|
|
769
|
+
}
|
|
770
|
+
if (m.importsRemoved.length > 0) {
|
|
771
|
+
console.log(` ${chalk.red('-imports:')} ${m.importsRemoved.join(', ')}`);
|
|
683
772
|
}
|
|
684
773
|
}
|
|
685
774
|
function signed(n) {
|
|
@@ -827,66 +916,87 @@ export function reportReview(report) {
|
|
|
827
916
|
console.log(`\n ${chalk.yellow('⚠')} ${report.reason ?? 'Review unavailable.'}\n`);
|
|
828
917
|
return;
|
|
829
918
|
}
|
|
919
|
+
printReviewRefs(report);
|
|
920
|
+
printReviewVerdict(report);
|
|
921
|
+
printReviewSummary(report);
|
|
922
|
+
printReviewChangedFiles(report);
|
|
923
|
+
printReviewCycles(report);
|
|
924
|
+
printReviewRiskyFunctions(report);
|
|
925
|
+
printReviewDependencyChanges(report);
|
|
926
|
+
}
|
|
927
|
+
function printReviewRefs(report) {
|
|
830
928
|
console.log(chalk.dim(`\n base ${report.base.ref} (${report.base.resolvedSha?.slice(0, 7) ?? '?'}) → head ${report.head.ref} (${report.head.resolvedSha?.slice(0, 7) ?? '?'})\n`));
|
|
929
|
+
}
|
|
930
|
+
function printReviewVerdict(report) {
|
|
831
931
|
const verdictColor = report.verdict === 'block' ? chalk.red : report.verdict === 'review' ? chalk.yellow : chalk.green;
|
|
832
932
|
const verdictLabel = report.verdict === 'block' ? '🚫 BLOCK' : report.verdict === 'review' ? '👀 REVIEW' : '✅ OK';
|
|
833
933
|
console.log(` ${chalk.bold('Verdict:')} ${verdictColor(verdictLabel)}\n`);
|
|
934
|
+
}
|
|
935
|
+
function printReviewSummary(report) {
|
|
834
936
|
for (const s of report.summary) {
|
|
835
937
|
console.log(` ${chalk.dim('•')} ${s}`);
|
|
836
938
|
}
|
|
837
939
|
if (report.summary.length > 0)
|
|
838
940
|
console.log('');
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
console.log('');
|
|
941
|
+
}
|
|
942
|
+
function printReviewChangedFiles(report) {
|
|
943
|
+
if (report.changedFiles.length === 0)
|
|
944
|
+
return;
|
|
945
|
+
console.log(chalk.bold(' Changed files (top by risk):'));
|
|
946
|
+
for (const f of report.changedFiles.slice(0, 15)) {
|
|
947
|
+
const risk = f.riskScore !== null ? f.riskScore.toFixed(1).padStart(6) : ' - ';
|
|
948
|
+
const cc = f.cyclomaticComplexity !== null ? String(f.cyclomaticComplexity).padStart(3) : ' -';
|
|
949
|
+
const dcc = f.cyclomaticDelta === null ? ' ' : signed(f.cyclomaticDelta).padStart(3);
|
|
950
|
+
const statusColor = f.status === 'added' ? chalk.green : f.status === 'removed' ? chalk.red : chalk.yellow;
|
|
951
|
+
console.log(` ${statusColor(f.status.padEnd(8))} risk ${risk} CC ${cc} (Δ${dcc}) ${chalk.cyan(f.relativePath)}`);
|
|
852
952
|
}
|
|
853
|
-
if (report.
|
|
854
|
-
console.log(chalk.
|
|
855
|
-
for (const c of report.newCycles.slice(0, 5)) {
|
|
856
|
-
const tag = c.classification === 'new' ? chalk.red('NEW') : chalk.yellow('EXP');
|
|
857
|
-
console.log(` ${tag} (${c.size}): ${c.files.join(' → ')}`);
|
|
858
|
-
}
|
|
859
|
-
if (report.newCycles.length > 5) {
|
|
860
|
-
console.log(chalk.dim(` ... and ${report.newCycles.length - 5} more`));
|
|
861
|
-
}
|
|
862
|
-
console.log('');
|
|
953
|
+
if (report.changedFiles.length > 15) {
|
|
954
|
+
console.log(chalk.dim(` ... and ${report.changedFiles.length - 15} more`));
|
|
863
955
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
console.log('');
|
|
956
|
+
console.log('');
|
|
957
|
+
}
|
|
958
|
+
function printReviewCycles(report) {
|
|
959
|
+
if (report.newCycles.length === 0)
|
|
960
|
+
return;
|
|
961
|
+
console.log(chalk.bold(` New / expanded cycles (${report.newCycles.length}):`));
|
|
962
|
+
for (const c of report.newCycles.slice(0, 5)) {
|
|
963
|
+
const tag = c.classification === 'new' ? chalk.red('NEW') : chalk.yellow('EXP');
|
|
964
|
+
console.log(` ${tag} (${c.size}): ${c.files.join(' → ')}`);
|
|
875
965
|
}
|
|
876
|
-
if (report.
|
|
877
|
-
console.log(chalk.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
console.log(
|
|
966
|
+
if (report.newCycles.length > 5) {
|
|
967
|
+
console.log(chalk.dim(` ... and ${report.newCycles.length - 5} more`));
|
|
968
|
+
}
|
|
969
|
+
console.log('');
|
|
970
|
+
}
|
|
971
|
+
function printReviewRiskyFunctions(report) {
|
|
972
|
+
if (report.riskyFunctions.length === 0)
|
|
973
|
+
return;
|
|
974
|
+
console.log(chalk.bold(` Risky functions (${report.riskyFunctions.length}):`));
|
|
975
|
+
for (const fn of report.riskyFunctions.slice(0, 10)) {
|
|
976
|
+
const cc = fn.cyclomaticComplexity >= 15 ? chalk.red : chalk.yellow;
|
|
977
|
+
const transition = fn.baseCc === null ? `(new)` : `(${fn.baseCc} → ${fn.cyclomaticComplexity})`;
|
|
978
|
+
console.log(` ${cc(`CC ${String(fn.cyclomaticComplexity).padStart(3)}`)} ${chalk.bold(fn.name)} ${chalk.dim(`${fn.file}:${fn.line}`)} ${chalk.dim(`[${fn.reason}] ${transition}`)}`);
|
|
979
|
+
}
|
|
980
|
+
if (report.riskyFunctions.length > 10) {
|
|
981
|
+
console.log(chalk.dim(` ... and ${report.riskyFunctions.length - 10} more`));
|
|
889
982
|
}
|
|
983
|
+
console.log('');
|
|
984
|
+
}
|
|
985
|
+
function printReviewDependencyChanges(report) {
|
|
986
|
+
if (report.dependencyChanges.length === 0)
|
|
987
|
+
return;
|
|
988
|
+
console.log(chalk.bold(' Dependency changes:'));
|
|
989
|
+
for (const d of report.dependencyChanges) {
|
|
990
|
+
const wsLabel = d.workspace ? ` (${d.workspace})` : '';
|
|
991
|
+
console.log(` ${chalk.cyan(d.manifestFile)}${chalk.dim(wsLabel)}`);
|
|
992
|
+
for (const a of d.added)
|
|
993
|
+
console.log(` ${chalk.green('+')} ${a.name}@${a.version} ${chalk.dim(`(${a.kind})`)}`);
|
|
994
|
+
for (const r of d.removed)
|
|
995
|
+
console.log(` ${chalk.red('-')} ${r.name}@${r.version} ${chalk.dim(`(${r.kind})`)}`);
|
|
996
|
+
for (const b of d.bumped)
|
|
997
|
+
console.log(` ${chalk.yellow('~')} ${b.name}: ${b.from} → ${b.to} ${chalk.dim(`(${b.kind})`)}`);
|
|
998
|
+
}
|
|
999
|
+
console.log('');
|
|
890
1000
|
}
|
|
891
1001
|
// ── Report: workspaces ────────────────────────────────────
|
|
892
1002
|
export function reportWorkspaces(info) {
|