@eduardbar/drift 0.2.3 → 0.3.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 +23 -0
- package/dist/cli.js +9 -2
- package/dist/printer.d.ts +3 -1
- package/dist/printer.js +49 -2
- package/dist/reporter.d.ts +2 -1
- package/dist/reporter.js +86 -0
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
- package/src/cli.ts +11 -3
- package/src/printer.ts +50 -3
- package/src/reporter.ts +94 -1
- package/src/types.ts +29 -0
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ npx @eduardbar/drift scan .
|
|
|
72
72
|
npx @eduardbar/drift scan ./src
|
|
73
73
|
npx @eduardbar/drift scan ./src --output report.md
|
|
74
74
|
npx @eduardbar/drift scan ./src --json
|
|
75
|
+
npx @eduardbar/drift scan ./src --ai
|
|
76
|
+
npx @eduardbar/drift scan ./src --fix
|
|
75
77
|
npx @eduardbar/drift scan ./src --min-score 50
|
|
76
78
|
|
|
77
79
|
# Install globally if you want the short 'drift' command
|
|
@@ -85,8 +87,29 @@ drift scan .
|
|
|
85
87
|
|------|-------------|
|
|
86
88
|
| `--output <file>` | Write Markdown report to a file |
|
|
87
89
|
| `--json` | Output raw JSON instead of console output |
|
|
90
|
+
| `--ai` | Output AI-optimized JSON for LLM consumption (Claude, GPT, etc.) |
|
|
91
|
+
| `--fix` | Show fix suggestions for each detected issue |
|
|
88
92
|
| `--min-score <n>` | Exit with code 1 if overall score exceeds threshold |
|
|
89
93
|
|
|
94
|
+
### AI Integration
|
|
95
|
+
|
|
96
|
+
Use `--ai` to get structured output that LLMs can consume:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx @eduardbar/drift scan ./src --ai
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Output includes:
|
|
103
|
+
- Priority-ordered issues (by severity and effort)
|
|
104
|
+
- Fix suggestions for each issue
|
|
105
|
+
- Recommended action for quick wins
|
|
106
|
+
|
|
107
|
+
Use `--fix` to see concrete fix suggestions in terminal:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @eduardbar/drift scan ./src --fix
|
|
111
|
+
```
|
|
112
|
+
|
|
90
113
|
---
|
|
91
114
|
|
|
92
115
|
## 🔍 What it detects
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { writeFileSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { analyzeProject } from './analyzer.js';
|
|
6
|
-
import { buildReport, formatMarkdown } from './reporter.js';
|
|
6
|
+
import { buildReport, formatMarkdown, formatAIOutput } from './reporter.js';
|
|
7
7
|
import { printConsole } from './printer.js';
|
|
8
8
|
const program = new Command();
|
|
9
9
|
program
|
|
@@ -15,6 +15,8 @@ program
|
|
|
15
15
|
.description('Scan a directory for vibe coding drift')
|
|
16
16
|
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
17
17
|
.option('--json', 'Output raw JSON report')
|
|
18
|
+
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
|
|
19
|
+
.option('--fix', 'Show fix suggestions for each issue')
|
|
18
20
|
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
19
21
|
.action((targetPath, options) => {
|
|
20
22
|
const resolvedPath = resolve(targetPath ?? '.');
|
|
@@ -22,11 +24,16 @@ program
|
|
|
22
24
|
const files = analyzeProject(resolvedPath);
|
|
23
25
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
|
|
24
26
|
const report = buildReport(resolvedPath, files);
|
|
27
|
+
if (options.ai) {
|
|
28
|
+
const aiOutput = formatAIOutput(report);
|
|
29
|
+
process.stdout.write(JSON.stringify(aiOutput, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
25
32
|
if (options.json) {
|
|
26
33
|
process.stdout.write(JSON.stringify(report, null, 2));
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
|
-
printConsole(report);
|
|
36
|
+
printConsole(report, { showFix: options.fix });
|
|
30
37
|
if (options.output) {
|
|
31
38
|
const md = formatMarkdown(report);
|
|
32
39
|
const outPath = resolve(options.output);
|
package/dist/printer.d.ts
CHANGED
package/dist/printer.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
// drift-ignore-file
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
import { scoreToGrade, severityIcon, scoreBar } from './utils.js';
|
|
4
|
-
|
|
4
|
+
function formatFixSuggestion(issue) {
|
|
5
|
+
const suggestions = {
|
|
6
|
+
'debug-leftover': [
|
|
7
|
+
'Remove this console.log statement',
|
|
8
|
+
'Or replace with a proper logging library',
|
|
9
|
+
],
|
|
10
|
+
'any-abuse': [
|
|
11
|
+
"Replace 'any' with 'unknown' for type safety",
|
|
12
|
+
'Or define a proper interface/type for this data',
|
|
13
|
+
],
|
|
14
|
+
'dead-code': [
|
|
15
|
+
'Remove this unused import',
|
|
16
|
+
],
|
|
17
|
+
'catch-swallow': [
|
|
18
|
+
'Add error handling: console.error(error) or logger.error(error)',
|
|
19
|
+
'Or re-throw if this should bubble up: throw error',
|
|
20
|
+
],
|
|
21
|
+
'large-function': [
|
|
22
|
+
'Extract logic into smaller functions',
|
|
23
|
+
'Each function should do one thing',
|
|
24
|
+
],
|
|
25
|
+
'large-file': [
|
|
26
|
+
'Split into multiple files by responsibility',
|
|
27
|
+
'Consider using a directory with index.ts',
|
|
28
|
+
],
|
|
29
|
+
'no-return-type': [
|
|
30
|
+
'Add explicit return type: function foo(): ReturnType',
|
|
31
|
+
],
|
|
32
|
+
'duplicate-function-name': [
|
|
33
|
+
'Consolidate with existing function',
|
|
34
|
+
'Or rename to clarify different behavior',
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
return suggestions[issue.rule] ?? ['Review and fix manually'];
|
|
38
|
+
}
|
|
39
|
+
export function printConsole(report, options) {
|
|
5
40
|
const sep = kleur.gray(' ' + '─'.repeat(50));
|
|
6
41
|
console.log();
|
|
7
42
|
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'));
|
|
@@ -49,7 +84,19 @@ export function printConsole(report) {
|
|
|
49
84
|
colorFn(issue.rule) +
|
|
50
85
|
` ` +
|
|
51
86
|
kleur.white(issue.message));
|
|
52
|
-
if (
|
|
87
|
+
if (options?.showFix) {
|
|
88
|
+
const fixes = formatFixSuggestion(issue);
|
|
89
|
+
console.log(kleur.gray(' ┌──────────────────────────────────────────────────────┐'));
|
|
90
|
+
if (issue.snippet) {
|
|
91
|
+
const line = issue.snippet.split('\n')[0].slice(0, 48);
|
|
92
|
+
console.log(kleur.gray(' │ ') + kleur.red('- ' + line));
|
|
93
|
+
}
|
|
94
|
+
for (const fix of fixes) {
|
|
95
|
+
console.log(kleur.gray(' │ ') + kleur.green('+ ' + fix));
|
|
96
|
+
}
|
|
97
|
+
console.log(kleur.gray(' └──────────────────────────────────────────────────────┘'));
|
|
98
|
+
}
|
|
99
|
+
else if (issue.snippet) {
|
|
53
100
|
const snippetIndent = ' ' + ' '.repeat(icon.length + 1);
|
|
54
101
|
console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`));
|
|
55
102
|
}
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { FileReport, DriftReport } from './types.js';
|
|
1
|
+
import type { FileReport, DriftReport, AIOutput } from './types.js';
|
|
2
2
|
export declare function buildReport(targetPath: string, files: FileReport[]): DriftReport;
|
|
3
3
|
export declare function formatMarkdown(report: DriftReport): string;
|
|
4
|
+
export declare function formatAIOutput(report: DriftReport): AIOutput;
|
|
4
5
|
//# sourceMappingURL=reporter.d.ts.map
|
package/dist/reporter.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { scoreToGradeText, severityIcon } from './utils.js';
|
|
2
|
+
const FIX_SUGGESTIONS = {
|
|
3
|
+
'large-file': 'Consider splitting this file into smaller modules with single responsibility',
|
|
4
|
+
'large-function': 'Extract logic into smaller functions with descriptive names',
|
|
5
|
+
'debug-leftover': 'Remove this console.log or replace with proper logging library',
|
|
6
|
+
'dead-code': 'Remove unused import to keep code clean',
|
|
7
|
+
'duplicate-function-name': 'Consolidate with existing function or rename to clarify different behavior',
|
|
8
|
+
'any-abuse': "Replace 'any' with proper type definition",
|
|
9
|
+
'catch-swallow': 'Add error handling or logging in catch block',
|
|
10
|
+
'no-return-type': 'Add explicit return type for better type safety',
|
|
11
|
+
};
|
|
12
|
+
const RULE_EFFORT = {
|
|
13
|
+
'debug-leftover': 'low',
|
|
14
|
+
'dead-code': 'low',
|
|
15
|
+
'no-return-type': 'low',
|
|
16
|
+
'any-abuse': 'medium',
|
|
17
|
+
'catch-swallow': 'medium',
|
|
18
|
+
'large-file': 'high',
|
|
19
|
+
'large-function': 'high',
|
|
20
|
+
'duplicate-function-name': 'high',
|
|
21
|
+
};
|
|
22
|
+
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
23
|
+
const EFFORT_ORDER = { low: 0, medium: 1, high: 2 };
|
|
2
24
|
export function buildReport(targetPath, files) {
|
|
3
25
|
const allIssues = files.flatMap((f) => f.issues);
|
|
4
26
|
const byRule = {};
|
|
@@ -83,4 +105,68 @@ export function formatMarkdown(report) {
|
|
|
83
105
|
}
|
|
84
106
|
return lines.join('\n');
|
|
85
107
|
}
|
|
108
|
+
function collectAllIssues(report) {
|
|
109
|
+
const all = [];
|
|
110
|
+
for (const file of report.files) {
|
|
111
|
+
for (const issue of file.issues) {
|
|
112
|
+
all.push({ file: file.path, issue });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return all;
|
|
116
|
+
}
|
|
117
|
+
function sortIssues(issues) {
|
|
118
|
+
return issues.sort((a, b) => {
|
|
119
|
+
const sevDiff = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity];
|
|
120
|
+
if (sevDiff !== 0)
|
|
121
|
+
return sevDiff;
|
|
122
|
+
const effortA = RULE_EFFORT[a.issue.rule] ?? 'medium';
|
|
123
|
+
const effortB = RULE_EFFORT[b.issue.rule] ?? 'medium';
|
|
124
|
+
return EFFORT_ORDER[effortA] - EFFORT_ORDER[effortB];
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function buildAIIssue(item, rank) {
|
|
128
|
+
return {
|
|
129
|
+
rank,
|
|
130
|
+
file: item.file,
|
|
131
|
+
line: item.issue.line,
|
|
132
|
+
rule: item.issue.rule,
|
|
133
|
+
severity: item.issue.severity,
|
|
134
|
+
message: item.issue.message,
|
|
135
|
+
snippet: item.issue.snippet,
|
|
136
|
+
fix_suggestion: FIX_SUGGESTIONS[item.issue.rule] ?? 'Review and fix this issue',
|
|
137
|
+
effort: RULE_EFFORT[item.issue.rule] ?? 'medium',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function buildRecommendedAction(priorityOrder) {
|
|
141
|
+
if (priorityOrder.length === 0)
|
|
142
|
+
return 'No issues detected. Codebase looks clean.';
|
|
143
|
+
const lowEffortCount = priorityOrder.filter((i) => i.effort === 'low').length;
|
|
144
|
+
if (lowEffortCount > 0) {
|
|
145
|
+
return `Focus on fixing ${lowEffortCount} low-effort issue(s) first - they're quick wins.`;
|
|
146
|
+
}
|
|
147
|
+
return 'Start with the highest priority issue and work through them in order.';
|
|
148
|
+
}
|
|
149
|
+
export function formatAIOutput(report) {
|
|
150
|
+
const allIssues = collectAllIssues(report);
|
|
151
|
+
const sortedIssues = sortIssues(allIssues);
|
|
152
|
+
const priorityOrder = sortedIssues.map((item, i) => buildAIIssue(item, i + 1));
|
|
153
|
+
const rulesDetected = [...new Set(allIssues.map((i) => i.issue.rule))];
|
|
154
|
+
const grade = scoreToGradeText(report.totalScore);
|
|
155
|
+
return {
|
|
156
|
+
summary: {
|
|
157
|
+
score: report.totalScore,
|
|
158
|
+
grade: grade.label.toUpperCase(),
|
|
159
|
+
total_issues: report.totalIssues,
|
|
160
|
+
files_affected: report.files.length,
|
|
161
|
+
files_clean: report.totalFiles - report.files.length,
|
|
162
|
+
},
|
|
163
|
+
priority_order: priorityOrder,
|
|
164
|
+
context_for_ai: {
|
|
165
|
+
project_type: 'typescript',
|
|
166
|
+
scan_path: report.targetPath,
|
|
167
|
+
rules_detected: rulesDetected,
|
|
168
|
+
recommended_action: buildRecommendedAction(priorityOrder),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
86
172
|
//# sourceMappingURL=reporter.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -25,4 +25,31 @@ export interface DriftReport {
|
|
|
25
25
|
byRule: Record<string, number>;
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
export interface AIOutput {
|
|
29
|
+
summary: {
|
|
30
|
+
score: number;
|
|
31
|
+
grade: string;
|
|
32
|
+
total_issues: number;
|
|
33
|
+
files_affected: number;
|
|
34
|
+
files_clean: number;
|
|
35
|
+
};
|
|
36
|
+
priority_order: AIIssue[];
|
|
37
|
+
context_for_ai: {
|
|
38
|
+
project_type: string;
|
|
39
|
+
scan_path: string;
|
|
40
|
+
rules_detected: string[];
|
|
41
|
+
recommended_action: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface AIIssue {
|
|
45
|
+
rank: number;
|
|
46
|
+
file: string;
|
|
47
|
+
line: number;
|
|
48
|
+
rule: string;
|
|
49
|
+
severity: string;
|
|
50
|
+
message: string;
|
|
51
|
+
snippet: string;
|
|
52
|
+
fix_suggestion: string;
|
|
53
|
+
effort: 'low' | 'medium' | 'high';
|
|
54
|
+
}
|
|
28
55
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander'
|
|
|
3
3
|
import { writeFileSync } from 'node:fs'
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
5
|
import { analyzeProject } from './analyzer.js'
|
|
6
|
-
import { buildReport, formatMarkdown } from './reporter.js'
|
|
6
|
+
import { buildReport, formatMarkdown, formatAIOutput } from './reporter.js'
|
|
7
7
|
import { printConsole } from './printer.js'
|
|
8
8
|
|
|
9
9
|
const program = new Command()
|
|
@@ -18,8 +18,10 @@ program
|
|
|
18
18
|
.description('Scan a directory for vibe coding drift')
|
|
19
19
|
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
20
20
|
.option('--json', 'Output raw JSON report')
|
|
21
|
+
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
|
|
22
|
+
.option('--fix', 'Show fix suggestions for each issue')
|
|
21
23
|
.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 }) => {
|
|
24
|
+
.action((targetPath: string | undefined, options: { output?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore: string }) => {
|
|
23
25
|
const resolvedPath = resolve(targetPath ?? '.')
|
|
24
26
|
|
|
25
27
|
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
@@ -27,12 +29,18 @@ program
|
|
|
27
29
|
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
28
30
|
const report = buildReport(resolvedPath, files)
|
|
29
31
|
|
|
32
|
+
if (options.ai) {
|
|
33
|
+
const aiOutput = formatAIOutput(report)
|
|
34
|
+
process.stdout.write(JSON.stringify(aiOutput, null, 2))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
if (options.json) {
|
|
31
39
|
process.stdout.write(JSON.stringify(report, null, 2))
|
|
32
40
|
return
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
printConsole(report)
|
|
43
|
+
printConsole(report, { showFix: options.fix })
|
|
36
44
|
|
|
37
45
|
if (options.output) {
|
|
38
46
|
const md = formatMarkdown(report)
|
package/src/printer.ts
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
// drift-ignore-file
|
|
2
2
|
import kleur from 'kleur'
|
|
3
|
-
import type { DriftReport } from './types.js'
|
|
3
|
+
import type { DriftIssue, DriftReport } from './types.js'
|
|
4
4
|
import { scoreToGrade, severityIcon, scoreBar } from './utils.js'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function formatFixSuggestion(issue: DriftIssue): string[] {
|
|
7
|
+
const suggestions: Record<string, string[]> = {
|
|
8
|
+
'debug-leftover': [
|
|
9
|
+
'Remove this console.log statement',
|
|
10
|
+
'Or replace with a proper logging library',
|
|
11
|
+
],
|
|
12
|
+
'any-abuse': [
|
|
13
|
+
"Replace 'any' with 'unknown' for type safety",
|
|
14
|
+
'Or define a proper interface/type for this data',
|
|
15
|
+
],
|
|
16
|
+
'dead-code': [
|
|
17
|
+
'Remove this unused import',
|
|
18
|
+
],
|
|
19
|
+
'catch-swallow': [
|
|
20
|
+
'Add error handling: console.error(error) or logger.error(error)',
|
|
21
|
+
'Or re-throw if this should bubble up: throw error',
|
|
22
|
+
],
|
|
23
|
+
'large-function': [
|
|
24
|
+
'Extract logic into smaller functions',
|
|
25
|
+
'Each function should do one thing',
|
|
26
|
+
],
|
|
27
|
+
'large-file': [
|
|
28
|
+
'Split into multiple files by responsibility',
|
|
29
|
+
'Consider using a directory with index.ts',
|
|
30
|
+
],
|
|
31
|
+
'no-return-type': [
|
|
32
|
+
'Add explicit return type: function foo(): ReturnType',
|
|
33
|
+
],
|
|
34
|
+
'duplicate-function-name': [
|
|
35
|
+
'Consolidate with existing function',
|
|
36
|
+
'Or rename to clarify different behavior',
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
return suggestions[issue.rule] ?? ['Review and fix manually']
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function printConsole(report: DriftReport, options?: { showFix?: boolean }): void {
|
|
7
43
|
const sep = kleur.gray(' ' + '─'.repeat(50))
|
|
8
44
|
|
|
9
45
|
console.log()
|
|
@@ -72,7 +108,18 @@ export function printConsole(report: DriftReport): void {
|
|
|
72
108
|
` ` +
|
|
73
109
|
kleur.white(issue.message)
|
|
74
110
|
)
|
|
75
|
-
if (
|
|
111
|
+
if (options?.showFix) {
|
|
112
|
+
const fixes = formatFixSuggestion(issue)
|
|
113
|
+
console.log(kleur.gray(' ┌──────────────────────────────────────────────────────┐'))
|
|
114
|
+
if (issue.snippet) {
|
|
115
|
+
const line = issue.snippet.split('\n')[0].slice(0, 48)
|
|
116
|
+
console.log(kleur.gray(' │ ') + kleur.red('- ' + line))
|
|
117
|
+
}
|
|
118
|
+
for (const fix of fixes) {
|
|
119
|
+
console.log(kleur.gray(' │ ') + kleur.green('+ ' + fix))
|
|
120
|
+
}
|
|
121
|
+
console.log(kleur.gray(' └──────────────────────────────────────────────────────┘'))
|
|
122
|
+
} else if (issue.snippet) {
|
|
76
123
|
const snippetIndent = ' ' + ' '.repeat(icon.length + 1)
|
|
77
124
|
console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`))
|
|
78
125
|
}
|
package/src/reporter.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import type { FileReport, DriftReport, DriftIssue } from './types.js'
|
|
1
|
+
import type { FileReport, DriftReport, DriftIssue, AIOutput, AIIssue } from './types.js'
|
|
2
2
|
import { scoreToGradeText, severityIcon } from './utils.js'
|
|
3
3
|
|
|
4
|
+
const FIX_SUGGESTIONS: Record<string, string> = {
|
|
5
|
+
'large-file': 'Consider splitting this file into smaller modules with single responsibility',
|
|
6
|
+
'large-function': 'Extract logic into smaller functions with descriptive names',
|
|
7
|
+
'debug-leftover': 'Remove this console.log or replace with proper logging library',
|
|
8
|
+
'dead-code': 'Remove unused import to keep code clean',
|
|
9
|
+
'duplicate-function-name': 'Consolidate with existing function or rename to clarify different behavior',
|
|
10
|
+
'any-abuse': "Replace 'any' with proper type definition",
|
|
11
|
+
'catch-swallow': 'Add error handling or logging in catch block',
|
|
12
|
+
'no-return-type': 'Add explicit return type for better type safety',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const RULE_EFFORT: Record<string, 'low' | 'medium' | 'high'> = {
|
|
16
|
+
'debug-leftover': 'low',
|
|
17
|
+
'dead-code': 'low',
|
|
18
|
+
'no-return-type': 'low',
|
|
19
|
+
'any-abuse': 'medium',
|
|
20
|
+
'catch-swallow': 'medium',
|
|
21
|
+
'large-file': 'high',
|
|
22
|
+
'large-function': 'high',
|
|
23
|
+
'duplicate-function-name': 'high',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SEVERITY_ORDER: Record<string, number> = { error: 0, warning: 1, info: 2 }
|
|
27
|
+
const EFFORT_ORDER: Record<string, number> = { low: 0, medium: 1, high: 2 }
|
|
28
|
+
|
|
4
29
|
export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
|
|
5
30
|
const allIssues = files.flatMap((f) => f.issues)
|
|
6
31
|
const byRule: Record<string, number> = {}
|
|
@@ -94,3 +119,71 @@ export function formatMarkdown(report: DriftReport): string {
|
|
|
94
119
|
|
|
95
120
|
return lines.join('\n')
|
|
96
121
|
}
|
|
122
|
+
|
|
123
|
+
function collectAllIssues(report: DriftReport): Array<{ file: string; issue: DriftIssue }> {
|
|
124
|
+
const all: Array<{ file: string; issue: DriftIssue }> = []
|
|
125
|
+
for (const file of report.files) {
|
|
126
|
+
for (const issue of file.issues) {
|
|
127
|
+
all.push({ file: file.path, issue })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return all
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sortIssues(issues: Array<{ file: string; issue: DriftIssue }>): Array<{ file: string; issue: DriftIssue }> {
|
|
134
|
+
return issues.sort((a, b) => {
|
|
135
|
+
const sevDiff = SEVERITY_ORDER[a.issue.severity] - SEVERITY_ORDER[b.issue.severity]
|
|
136
|
+
if (sevDiff !== 0) return sevDiff
|
|
137
|
+
const effortA = RULE_EFFORT[a.issue.rule] ?? 'medium'
|
|
138
|
+
const effortB = RULE_EFFORT[b.issue.rule] ?? 'medium'
|
|
139
|
+
return EFFORT_ORDER[effortA] - EFFORT_ORDER[effortB]
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildAIIssue(item: { file: string; issue: DriftIssue }, rank: number): AIIssue {
|
|
144
|
+
return {
|
|
145
|
+
rank,
|
|
146
|
+
file: item.file,
|
|
147
|
+
line: item.issue.line,
|
|
148
|
+
rule: item.issue.rule,
|
|
149
|
+
severity: item.issue.severity,
|
|
150
|
+
message: item.issue.message,
|
|
151
|
+
snippet: item.issue.snippet,
|
|
152
|
+
fix_suggestion: FIX_SUGGESTIONS[item.issue.rule] ?? 'Review and fix this issue',
|
|
153
|
+
effort: RULE_EFFORT[item.issue.rule] ?? 'medium',
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildRecommendedAction(priorityOrder: AIIssue[]): string {
|
|
158
|
+
if (priorityOrder.length === 0) return 'No issues detected. Codebase looks clean.'
|
|
159
|
+
const lowEffortCount = priorityOrder.filter((i) => i.effort === 'low').length
|
|
160
|
+
if (lowEffortCount > 0) {
|
|
161
|
+
return `Focus on fixing ${lowEffortCount} low-effort issue(s) first - they're quick wins.`
|
|
162
|
+
}
|
|
163
|
+
return 'Start with the highest priority issue and work through them in order.'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatAIOutput(report: DriftReport): AIOutput {
|
|
167
|
+
const allIssues = collectAllIssues(report)
|
|
168
|
+
const sortedIssues = sortIssues(allIssues)
|
|
169
|
+
const priorityOrder = sortedIssues.map((item, i) => buildAIIssue(item, i + 1))
|
|
170
|
+
const rulesDetected = [...new Set(allIssues.map((i) => i.issue.rule))]
|
|
171
|
+
const grade = scoreToGradeText(report.totalScore)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
summary: {
|
|
175
|
+
score: report.totalScore,
|
|
176
|
+
grade: grade.label.toUpperCase(),
|
|
177
|
+
total_issues: report.totalIssues,
|
|
178
|
+
files_affected: report.files.length,
|
|
179
|
+
files_clean: report.totalFiles - report.files.length,
|
|
180
|
+
},
|
|
181
|
+
priority_order: priorityOrder,
|
|
182
|
+
context_for_ai: {
|
|
183
|
+
project_type: 'typescript',
|
|
184
|
+
scan_path: report.targetPath,
|
|
185
|
+
rules_detected: rulesDetected,
|
|
186
|
+
recommended_action: buildRecommendedAction(priorityOrder),
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -27,3 +27,32 @@ export interface DriftReport {
|
|
|
27
27
|
byRule: Record<string, number>
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export interface AIOutput {
|
|
32
|
+
summary: {
|
|
33
|
+
score: number
|
|
34
|
+
grade: string
|
|
35
|
+
total_issues: number
|
|
36
|
+
files_affected: number
|
|
37
|
+
files_clean: number
|
|
38
|
+
}
|
|
39
|
+
priority_order: AIIssue[]
|
|
40
|
+
context_for_ai: {
|
|
41
|
+
project_type: string
|
|
42
|
+
scan_path: string
|
|
43
|
+
rules_detected: string[]
|
|
44
|
+
recommended_action: string
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AIIssue {
|
|
49
|
+
rank: number
|
|
50
|
+
file: string
|
|
51
|
+
line: number
|
|
52
|
+
rule: string
|
|
53
|
+
severity: string
|
|
54
|
+
message: string
|
|
55
|
+
snippet: string
|
|
56
|
+
fix_suggestion: string
|
|
57
|
+
effort: 'low' | 'medium' | 'high'
|
|
58
|
+
}
|