@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 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
@@ -1,3 +1,5 @@
1
1
  import type { DriftReport } from './types.js';
2
- export declare function printConsole(report: DriftReport): void;
2
+ export declare function printConsole(report: DriftReport, options?: {
3
+ showFix?: boolean;
4
+ }): void;
3
5
  //# sourceMappingURL=printer.d.ts.map
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
- export function printConsole(report) {
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 (issue.snippet) {
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardbar/drift",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Detect silent technical debt left by AI-generated code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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
- export function printConsole(report: DriftReport): void {
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 (issue.snippet) {
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
+ }