@eduardbar/drift 0.1.0 → 0.2.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/assets/og.png +0 -0
- package/dist/cli.js +2 -1
- package/dist/printer.js +19 -31
- package/dist/reporter.js +4 -20
- package/dist/types.d.ts +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +36 -0
- package/package.json +9 -2
- package/src/cli.ts +50 -50
- package/src/printer.ts +81 -86
- package/src/reporter.ts +85 -97
- package/src/types.ts +29 -28
- package/src/utils.ts +35 -0
package/assets/og.png
ADDED
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -18,8 +18,9 @@ program
|
|
|
18
18
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
19
19
|
.action((targetPath, options) => {
|
|
20
20
|
const resolvedPath = resolve(targetPath ?? '.');
|
|
21
|
-
|
|
21
|
+
process.stderr.write(`\nScanning ${resolvedPath}...\n`);
|
|
22
22
|
const files = analyzeProject(resolvedPath);
|
|
23
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
|
|
23
24
|
const report = buildReport(resolvedPath, files);
|
|
24
25
|
if (options.json) {
|
|
25
26
|
process.stdout.write(JSON.stringify(report, null, 2));
|
package/dist/printer.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
|
+
import { scoreToGrade, severityIcon, scoreBar } from './utils.js';
|
|
2
3
|
export function printConsole(report) {
|
|
4
|
+
const sep = kleur.gray(' ' + '─'.repeat(50));
|
|
3
5
|
console.log();
|
|
4
|
-
console.log(kleur.bold().white(' drift') + kleur.gray('
|
|
6
|
+
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'));
|
|
7
|
+
console.log(sep);
|
|
5
8
|
console.log();
|
|
6
9
|
const grade = scoreToGrade(report.totalScore);
|
|
7
10
|
const scoreColor = report.totalScore === 0
|
|
@@ -9,8 +12,19 @@ export function printConsole(report) {
|
|
|
9
12
|
: report.totalScore < 45
|
|
10
13
|
? kleur.yellow
|
|
11
14
|
: kleur.red;
|
|
12
|
-
|
|
13
|
-
console.log(
|
|
15
|
+
const bar = scoreBar(report.totalScore);
|
|
16
|
+
console.log(` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`);
|
|
17
|
+
const cleanFiles = report.totalFiles - report.files.length;
|
|
18
|
+
console.log(kleur.gray(` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`));
|
|
19
|
+
console.log();
|
|
20
|
+
// Top issues in header
|
|
21
|
+
const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
22
|
+
if (topRules.length > 0) {
|
|
23
|
+
const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`);
|
|
24
|
+
console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`);
|
|
25
|
+
console.log();
|
|
26
|
+
}
|
|
27
|
+
console.log(sep);
|
|
14
28
|
console.log();
|
|
15
29
|
if (report.files.length === 0) {
|
|
16
30
|
console.log(kleur.green(' No drift detected. Clean codebase.'));
|
|
@@ -35,37 +49,11 @@ export function printConsole(report) {
|
|
|
35
49
|
` ` +
|
|
36
50
|
kleur.white(issue.message));
|
|
37
51
|
if (issue.snippet) {
|
|
38
|
-
|
|
52
|
+
const snippetIndent = ' ' + ' '.repeat(icon.length + 1);
|
|
53
|
+
console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`));
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
console.log();
|
|
42
57
|
}
|
|
43
|
-
// Top drifting rules summary
|
|
44
|
-
const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
45
|
-
if (sorted.length > 0) {
|
|
46
|
-
console.log(kleur.gray(' Top rules:'));
|
|
47
|
-
for (const [rule, count] of sorted) {
|
|
48
|
-
console.log(kleur.gray(` · ${rule}: ${count}`));
|
|
49
|
-
}
|
|
50
|
-
console.log();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function scoreToGrade(score) {
|
|
54
|
-
if (score === 0)
|
|
55
|
-
return { badge: kleur.green('CLEAN') };
|
|
56
|
-
if (score < 20)
|
|
57
|
-
return { badge: kleur.green('LOW') };
|
|
58
|
-
if (score < 45)
|
|
59
|
-
return { badge: kleur.yellow('MODERATE') };
|
|
60
|
-
if (score < 70)
|
|
61
|
-
return { badge: kleur.red('HIGH') };
|
|
62
|
-
return { badge: kleur.bold().red('CRITICAL') };
|
|
63
|
-
}
|
|
64
|
-
function severityIcon(s) {
|
|
65
|
-
if (s === 'error')
|
|
66
|
-
return '✖';
|
|
67
|
-
if (s === 'warning')
|
|
68
|
-
return '▲';
|
|
69
|
-
return '◦';
|
|
70
58
|
}
|
|
71
59
|
//# sourceMappingURL=printer.js.map
|
package/dist/reporter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scoreToGradeText, severityIcon } from './utils.js';
|
|
1
2
|
export function buildReport(targetPath, files) {
|
|
2
3
|
const allIssues = files.flatMap((f) => f.issues);
|
|
3
4
|
const byRule = {};
|
|
@@ -13,6 +14,7 @@ export function buildReport(targetPath, files) {
|
|
|
13
14
|
files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
|
|
14
15
|
totalIssues: allIssues.length,
|
|
15
16
|
totalScore,
|
|
17
|
+
totalFiles: files.length,
|
|
16
18
|
summary: {
|
|
17
19
|
errors: allIssues.filter((i) => i.severity === 'error').length,
|
|
18
20
|
warnings: allIssues.filter((i) => i.severity === 'warning').length,
|
|
@@ -22,7 +24,7 @@ export function buildReport(targetPath, files) {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
export function formatMarkdown(report) {
|
|
25
|
-
const grade =
|
|
27
|
+
const grade = scoreToGradeText(report.totalScore);
|
|
26
28
|
const lines = [];
|
|
27
29
|
lines.push(`# drift report`);
|
|
28
30
|
lines.push(``);
|
|
@@ -62,7 +64,7 @@ export function formatMarkdown(report) {
|
|
|
62
64
|
for (const issue of file.issues) {
|
|
63
65
|
const icon = severityIcon(issue.severity);
|
|
64
66
|
lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`);
|
|
65
|
-
lines.push(
|
|
67
|
+
lines.push(`\`\`\`typescript`);
|
|
66
68
|
lines.push(issue.snippet);
|
|
67
69
|
lines.push(`\`\`\``);
|
|
68
70
|
lines.push(``);
|
|
@@ -71,22 +73,4 @@ export function formatMarkdown(report) {
|
|
|
71
73
|
}
|
|
72
74
|
return lines.join('\n');
|
|
73
75
|
}
|
|
74
|
-
function scoreToGrade(score) {
|
|
75
|
-
if (score === 0)
|
|
76
|
-
return { badge: '✦ CLEAN', label: 'clean' };
|
|
77
|
-
if (score < 20)
|
|
78
|
-
return { badge: '◎ LOW', label: 'low' };
|
|
79
|
-
if (score < 45)
|
|
80
|
-
return { badge: '◈ MODERATE', label: 'moderate' };
|
|
81
|
-
if (score < 70)
|
|
82
|
-
return { badge: '◉ HIGH', label: 'high' };
|
|
83
|
-
return { badge: '⬡ CRITICAL', label: 'critical' };
|
|
84
|
-
}
|
|
85
|
-
function severityIcon(s) {
|
|
86
|
-
if (s === 'error')
|
|
87
|
-
return '✖';
|
|
88
|
-
if (s === 'warning')
|
|
89
|
-
return '▲';
|
|
90
|
-
return '◦';
|
|
91
|
-
}
|
|
92
76
|
//# sourceMappingURL=reporter.js.map
|
package/dist/types.d.ts
CHANGED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DriftIssue } from './types.js';
|
|
2
|
+
export interface Grade {
|
|
3
|
+
badge: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function scoreToGrade(score: number): Grade;
|
|
7
|
+
export declare function scoreToGradeText(score: number): Grade;
|
|
8
|
+
export declare function severityIcon(s: DriftIssue['severity']): string;
|
|
9
|
+
export declare function scoreBar(score: number, width?: number): string;
|
|
10
|
+
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
export function scoreToGrade(score) {
|
|
3
|
+
if (score === 0)
|
|
4
|
+
return { badge: kleur.green('CLEAN'), label: 'clean' };
|
|
5
|
+
if (score < 20)
|
|
6
|
+
return { badge: kleur.green('LOW'), label: 'low' };
|
|
7
|
+
if (score < 45)
|
|
8
|
+
return { badge: kleur.yellow('MODERATE'), label: 'moderate' };
|
|
9
|
+
if (score < 70)
|
|
10
|
+
return { badge: kleur.red('HIGH'), label: 'high' };
|
|
11
|
+
return { badge: kleur.bold().red('CRITICAL'), label: 'critical' };
|
|
12
|
+
}
|
|
13
|
+
export function scoreToGradeText(score) {
|
|
14
|
+
if (score === 0)
|
|
15
|
+
return { badge: '✦ CLEAN', label: 'clean' };
|
|
16
|
+
if (score < 20)
|
|
17
|
+
return { badge: '◎ LOW', label: 'low' };
|
|
18
|
+
if (score < 45)
|
|
19
|
+
return { badge: '◈ MODERATE', label: 'moderate' };
|
|
20
|
+
if (score < 70)
|
|
21
|
+
return { badge: '◉ HIGH', label: 'high' };
|
|
22
|
+
return { badge: '⬡ CRITICAL', label: 'critical' };
|
|
23
|
+
}
|
|
24
|
+
export function severityIcon(s) {
|
|
25
|
+
if (s === 'error')
|
|
26
|
+
return '✖';
|
|
27
|
+
if (s === 'warning')
|
|
28
|
+
return '▲';
|
|
29
|
+
return '◦';
|
|
30
|
+
}
|
|
31
|
+
export function scoreBar(score, width = 20) {
|
|
32
|
+
const filled = Math.round((score / 100) * width);
|
|
33
|
+
const empty = width - filled;
|
|
34
|
+
return '█'.repeat(filled) + '░'.repeat(empty);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eduardbar/drift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Detect silent technical debt left by AI-generated code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"start": "node dist/cli.js",
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vibe-coding",
|
|
18
|
+
"technical-debt",
|
|
19
|
+
"ai",
|
|
20
|
+
"cli",
|
|
21
|
+
"typescript",
|
|
22
|
+
"static-analysis"
|
|
23
|
+
],
|
|
17
24
|
"author": "eduardbar",
|
|
18
25
|
"license": "MIT",
|
|
19
26
|
"dependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander'
|
|
3
|
-
import { writeFileSync } from 'node:fs'
|
|
4
|
-
import { resolve } from 'node:path'
|
|
5
|
-
import { analyzeProject } from './analyzer.js'
|
|
6
|
-
import { buildReport, formatMarkdown } from './reporter.js'
|
|
7
|
-
import { printConsole } from './printer.js'
|
|
8
|
-
|
|
9
|
-
const program = new Command()
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.name('drift')
|
|
13
|
-
.description('Detect silent technical debt left by AI-generated code')
|
|
14
|
-
.version('0.1.0')
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.command('scan [path]', { isDefault: true })
|
|
18
|
-
.description('Scan a directory for vibe coding drift')
|
|
19
|
-
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
20
|
-
.option('--json', 'Output raw JSON report')
|
|
21
|
-
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
22
|
-
.action((targetPath: string | undefined, options: { output?: string; json?: boolean; minScore: string }) => {
|
|
23
|
-
const resolvedPath = resolve(targetPath ?? '.')
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const report = buildReport(resolvedPath, files)
|
|
29
|
-
|
|
30
|
-
if (options.json) {
|
|
31
|
-
process.stdout.write(JSON.stringify(report, null, 2))
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
printConsole(report)
|
|
36
|
-
|
|
37
|
-
if (options.output) {
|
|
38
|
-
const md = formatMarkdown(report)
|
|
39
|
-
const outPath = resolve(options.output)
|
|
40
|
-
writeFileSync(outPath, md, 'utf8')
|
|
41
|
-
console.error(`Report saved to ${outPath}`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const minScore = Number(options.minScore)
|
|
45
|
-
if (minScore > 0 && report.totalScore > minScore) {
|
|
46
|
-
process.exit(1)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
program.parse()
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { writeFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { analyzeProject } from './analyzer.js'
|
|
6
|
+
import { buildReport, formatMarkdown } from './reporter.js'
|
|
7
|
+
import { printConsole } from './printer.js'
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('drift')
|
|
13
|
+
.description('Detect silent technical debt left by AI-generated code')
|
|
14
|
+
.version('0.1.0')
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('scan [path]', { isDefault: true })
|
|
18
|
+
.description('Scan a directory for vibe coding drift')
|
|
19
|
+
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
20
|
+
.option('--json', 'Output raw JSON report')
|
|
21
|
+
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
22
|
+
.action((targetPath: string | undefined, options: { output?: string; json?: boolean; minScore: string }) => {
|
|
23
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
24
|
+
|
|
25
|
+
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
26
|
+
const files = analyzeProject(resolvedPath)
|
|
27
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
28
|
+
const report = buildReport(resolvedPath, files)
|
|
29
|
+
|
|
30
|
+
if (options.json) {
|
|
31
|
+
process.stdout.write(JSON.stringify(report, null, 2))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
printConsole(report)
|
|
36
|
+
|
|
37
|
+
if (options.output) {
|
|
38
|
+
const md = formatMarkdown(report)
|
|
39
|
+
const outPath = resolve(options.output)
|
|
40
|
+
writeFileSync(outPath, md, 'utf8')
|
|
41
|
+
console.error(`Report saved to ${outPath}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const minScore = Number(options.minScore)
|
|
45
|
+
if (minScore > 0 && report.totalScore > minScore) {
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
program.parse()
|
package/src/printer.ts
CHANGED
|
@@ -1,86 +1,81 @@
|
|
|
1
|
-
import kleur from 'kleur'
|
|
2
|
-
import type { DriftReport
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function severityIcon(s: DriftIssue['severity']): string {
|
|
83
|
-
if (s === 'error') return '✖'
|
|
84
|
-
if (s === 'warning') return '▲'
|
|
85
|
-
return '◦'
|
|
86
|
-
}
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import type { DriftReport } from './types.js'
|
|
3
|
+
import { scoreToGrade, severityIcon, scoreBar } from './utils.js'
|
|
4
|
+
|
|
5
|
+
export function printConsole(report: DriftReport): void {
|
|
6
|
+
const sep = kleur.gray(' ' + '─'.repeat(50))
|
|
7
|
+
|
|
8
|
+
console.log()
|
|
9
|
+
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'))
|
|
10
|
+
console.log(sep)
|
|
11
|
+
console.log()
|
|
12
|
+
|
|
13
|
+
const grade = scoreToGrade(report.totalScore)
|
|
14
|
+
const scoreColor = report.totalScore === 0
|
|
15
|
+
? kleur.green
|
|
16
|
+
: report.totalScore < 45
|
|
17
|
+
? kleur.yellow
|
|
18
|
+
: kleur.red
|
|
19
|
+
|
|
20
|
+
const bar = scoreBar(report.totalScore)
|
|
21
|
+
console.log(
|
|
22
|
+
` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const cleanFiles = report.totalFiles - report.files.length
|
|
26
|
+
console.log(
|
|
27
|
+
kleur.gray(
|
|
28
|
+
` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
console.log()
|
|
32
|
+
|
|
33
|
+
// Top issues in header
|
|
34
|
+
const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
35
|
+
if (topRules.length > 0) {
|
|
36
|
+
const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`)
|
|
37
|
+
console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`)
|
|
38
|
+
console.log()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(sep)
|
|
42
|
+
console.log()
|
|
43
|
+
|
|
44
|
+
if (report.files.length === 0) {
|
|
45
|
+
console.log(kleur.green(' No drift detected. Clean codebase.'))
|
|
46
|
+
console.log()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const file of report.files) {
|
|
51
|
+
const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '')
|
|
52
|
+
console.log(
|
|
53
|
+
kleur.bold().white(` ${rel}`) +
|
|
54
|
+
kleur.gray(` (score ${file.score}/100)`)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for (const issue of file.issues) {
|
|
58
|
+
const icon = severityIcon(issue.severity)
|
|
59
|
+
const colorFn = (s: string) =>
|
|
60
|
+
issue.severity === 'error'
|
|
61
|
+
? kleur.red(s)
|
|
62
|
+
: issue.severity === 'warning'
|
|
63
|
+
? kleur.yellow(s)
|
|
64
|
+
: kleur.cyan(s)
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
` ${colorFn(icon)} ` +
|
|
68
|
+
kleur.gray(`L${issue.line}`) +
|
|
69
|
+
` ` +
|
|
70
|
+
colorFn(issue.rule) +
|
|
71
|
+
` ` +
|
|
72
|
+
kleur.white(issue.message)
|
|
73
|
+
)
|
|
74
|
+
if (issue.snippet) {
|
|
75
|
+
const snippetIndent = ' ' + ' '.repeat(icon.length + 1)
|
|
76
|
+
console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log()
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/reporter.ts
CHANGED
|
@@ -1,97 +1,85 @@
|
|
|
1
|
-
import type { FileReport, DriftReport
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
lines
|
|
36
|
-
|
|
37
|
-
lines.push(
|
|
38
|
-
lines.push(
|
|
39
|
-
lines.push(
|
|
40
|
-
lines.push(
|
|
41
|
-
lines.push(``)
|
|
42
|
-
lines.push(
|
|
43
|
-
lines.push(
|
|
44
|
-
lines.push(`|
|
|
45
|
-
lines.push(
|
|
46
|
-
lines.push(`|
|
|
47
|
-
lines.push(`|
|
|
48
|
-
lines.push(`|
|
|
49
|
-
lines.push(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
lines.push(
|
|
65
|
-
|
|
66
|
-
lines.push(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
lines.push(issue.
|
|
76
|
-
lines.push(
|
|
77
|
-
lines.push(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (score === 0) return { badge: '✦ CLEAN', label: 'clean' }
|
|
87
|
-
if (score < 20) return { badge: '◎ LOW', label: 'low' }
|
|
88
|
-
if (score < 45) return { badge: '◈ MODERATE', label: 'moderate' }
|
|
89
|
-
if (score < 70) return { badge: '◉ HIGH', label: 'high' }
|
|
90
|
-
return { badge: '⬡ CRITICAL', label: 'critical' }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function severityIcon(s: DriftIssue['severity']): string {
|
|
94
|
-
if (s === 'error') return '✖'
|
|
95
|
-
if (s === 'warning') return '▲'
|
|
96
|
-
return '◦'
|
|
97
|
-
}
|
|
1
|
+
import type { FileReport, DriftReport } from './types.js'
|
|
2
|
+
import { scoreToGradeText, severityIcon } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
|
|
5
|
+
const allIssues = files.flatMap((f) => f.issues)
|
|
6
|
+
const byRule: Record<string, number> = {}
|
|
7
|
+
|
|
8
|
+
for (const issue of allIssues) {
|
|
9
|
+
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const totalScore =
|
|
13
|
+
files.length > 0
|
|
14
|
+
? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
|
|
15
|
+
: 0
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
scannedAt: new Date().toISOString(),
|
|
19
|
+
targetPath,
|
|
20
|
+
files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
|
|
21
|
+
totalIssues: allIssues.length,
|
|
22
|
+
totalScore,
|
|
23
|
+
totalFiles: files.length,
|
|
24
|
+
summary: {
|
|
25
|
+
errors: allIssues.filter((i) => i.severity === 'error').length,
|
|
26
|
+
warnings: allIssues.filter((i) => i.severity === 'warning').length,
|
|
27
|
+
infos: allIssues.filter((i) => i.severity === 'info').length,
|
|
28
|
+
byRule,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatMarkdown(report: DriftReport): string {
|
|
34
|
+
const grade = scoreToGradeText(report.totalScore)
|
|
35
|
+
const lines: string[] = []
|
|
36
|
+
|
|
37
|
+
lines.push(`# drift report`)
|
|
38
|
+
lines.push(``)
|
|
39
|
+
lines.push(`> Generated: ${new Date(report.scannedAt).toLocaleString()}`)
|
|
40
|
+
lines.push(`> Path: \`${report.targetPath}\``)
|
|
41
|
+
lines.push(``)
|
|
42
|
+
lines.push(`## Overall drift score: ${report.totalScore}/100 ${grade.badge}`)
|
|
43
|
+
lines.push(``)
|
|
44
|
+
lines.push(`| | Count |`)
|
|
45
|
+
lines.push(`|---|---|`)
|
|
46
|
+
lines.push(`| Errors | ${report.summary.errors} |`)
|
|
47
|
+
lines.push(`| Warnings | ${report.summary.warnings} |`)
|
|
48
|
+
lines.push(`| Info | ${report.summary.infos} |`)
|
|
49
|
+
lines.push(`| Files with issues | ${report.files.length} |`)
|
|
50
|
+
lines.push(`| Total issues | ${report.totalIssues} |`)
|
|
51
|
+
lines.push(``)
|
|
52
|
+
|
|
53
|
+
if (Object.keys(report.summary.byRule).length > 0) {
|
|
54
|
+
lines.push(`## Issues by rule`)
|
|
55
|
+
lines.push(``)
|
|
56
|
+
const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1])
|
|
57
|
+
for (const [rule, count] of sorted) {
|
|
58
|
+
lines.push(`- \`${rule}\`: ${count}`)
|
|
59
|
+
}
|
|
60
|
+
lines.push(``)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (report.files.length === 0) {
|
|
64
|
+
lines.push(`## No drift detected`)
|
|
65
|
+
lines.push(``)
|
|
66
|
+
lines.push(`No issues found. Clean codebase.`)
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(`## Files (sorted by drift score)`)
|
|
69
|
+
lines.push(``)
|
|
70
|
+
for (const file of report.files) {
|
|
71
|
+
lines.push(`### \`${file.path}\` — score ${file.score}/100`)
|
|
72
|
+
lines.push(``)
|
|
73
|
+
for (const issue of file.issues) {
|
|
74
|
+
const icon = severityIcon(issue.severity)
|
|
75
|
+
lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`)
|
|
76
|
+
lines.push(`\`\`\`typescript`)
|
|
77
|
+
lines.push(issue.snippet)
|
|
78
|
+
lines.push(`\`\`\``)
|
|
79
|
+
lines.push(``)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n')
|
|
85
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
export interface DriftIssue {
|
|
2
|
-
rule: string
|
|
3
|
-
severity: 'error' | 'warning' | 'info'
|
|
4
|
-
message: string
|
|
5
|
-
line: number
|
|
6
|
-
column: number
|
|
7
|
-
snippet: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface FileReport {
|
|
11
|
-
path: string
|
|
12
|
-
issues: DriftIssue[]
|
|
13
|
-
score: number // 0–100, higher = more drift
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface DriftReport {
|
|
17
|
-
scannedAt: string
|
|
18
|
-
targetPath: string
|
|
19
|
-
files: FileReport[]
|
|
20
|
-
totalIssues: number
|
|
21
|
-
totalScore: number
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
1
|
+
export interface DriftIssue {
|
|
2
|
+
rule: string
|
|
3
|
+
severity: 'error' | 'warning' | 'info'
|
|
4
|
+
message: string
|
|
5
|
+
line: number
|
|
6
|
+
column: number
|
|
7
|
+
snippet: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FileReport {
|
|
11
|
+
path: string
|
|
12
|
+
issues: DriftIssue[]
|
|
13
|
+
score: number // 0–100, higher = more drift
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DriftReport {
|
|
17
|
+
scannedAt: string
|
|
18
|
+
targetPath: string
|
|
19
|
+
files: FileReport[]
|
|
20
|
+
totalIssues: number
|
|
21
|
+
totalScore: number
|
|
22
|
+
totalFiles: number
|
|
23
|
+
summary: {
|
|
24
|
+
errors: number
|
|
25
|
+
warnings: number
|
|
26
|
+
infos: number
|
|
27
|
+
byRule: Record<string, number>
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import type { DriftIssue } from './types.js'
|
|
3
|
+
|
|
4
|
+
export interface Grade {
|
|
5
|
+
badge: string
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function scoreToGrade(score: number): Grade {
|
|
10
|
+
if (score === 0) return { badge: kleur.green('CLEAN'), label: 'clean' }
|
|
11
|
+
if (score < 20) return { badge: kleur.green('LOW'), label: 'low' }
|
|
12
|
+
if (score < 45) return { badge: kleur.yellow('MODERATE'), label: 'moderate' }
|
|
13
|
+
if (score < 70) return { badge: kleur.red('HIGH'), label: 'high' }
|
|
14
|
+
return { badge: kleur.bold().red('CRITICAL'), label: 'critical' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function scoreToGradeText(score: number): Grade {
|
|
18
|
+
if (score === 0) return { badge: '✦ CLEAN', label: 'clean' }
|
|
19
|
+
if (score < 20) return { badge: '◎ LOW', label: 'low' }
|
|
20
|
+
if (score < 45) return { badge: '◈ MODERATE', label: 'moderate' }
|
|
21
|
+
if (score < 70) return { badge: '◉ HIGH', label: 'high' }
|
|
22
|
+
return { badge: '⬡ CRITICAL', label: 'critical' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function severityIcon(s: DriftIssue['severity']): string {
|
|
26
|
+
if (s === 'error') return '✖'
|
|
27
|
+
if (s === 'warning') return '▲'
|
|
28
|
+
return '◦'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function scoreBar(score: number, width = 20): string {
|
|
32
|
+
const filled = Math.round((score / 100) * width)
|
|
33
|
+
const empty = width - filled
|
|
34
|
+
return '█'.repeat(filled) + '░'.repeat(empty)
|
|
35
|
+
}
|