@eduardbar/drift 0.7.0 → 0.9.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/CHANGELOG.md +26 -2
- package/assets/og.svg +105 -105
- package/dist/analyzer.d.ts +36 -1
- package/dist/analyzer.js +547 -0
- package/dist/cli.js +39 -1
- package/dist/report.js +3 -1
- package/dist/types.d.ts +39 -0
- package/package.json +1 -1
- package/src/analyzer.ts +661 -1
- package/src/cli.ts +47 -1
- package/src/printer.ts +60 -60
- package/src/report.ts +3 -1
- package/src/types.ts +59 -15
- package/src/utils.ts +35 -35
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import { writeFileSync } from 'node:fs'
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
const require = createRequire(import.meta.url)
|
|
7
|
+
const { version: VERSION } = require('../package.json') as { version: string }
|
|
5
8
|
import { analyzeProject } from './analyzer.js'
|
|
6
9
|
import { buildReport, formatMarkdown, formatAIOutput } from './reporter.js'
|
|
7
10
|
import { printConsole, printDiff } from './printer.js'
|
|
@@ -11,13 +14,14 @@ import { computeDiff } from './diff.js'
|
|
|
11
14
|
import { generateHtmlReport } from './report.js'
|
|
12
15
|
import { generateBadge } from './badge.js'
|
|
13
16
|
import { emitCIAnnotations, printCISummary } from './ci.js'
|
|
17
|
+
import { TrendAnalyzer, BlameAnalyzer } from './analyzer.js'
|
|
14
18
|
|
|
15
19
|
const program = new Command()
|
|
16
20
|
|
|
17
21
|
program
|
|
18
22
|
.name('drift')
|
|
19
23
|
.description('Detect silent technical debt left by AI-generated code')
|
|
20
|
-
.version(
|
|
24
|
+
.version(VERSION)
|
|
21
25
|
|
|
22
26
|
program
|
|
23
27
|
.command('scan [path]', { isDefault: true })
|
|
@@ -163,4 +167,46 @@ program
|
|
|
163
167
|
}
|
|
164
168
|
})
|
|
165
169
|
|
|
170
|
+
program
|
|
171
|
+
.command('trend [period]')
|
|
172
|
+
.description('Analyze trend of technical debt over time')
|
|
173
|
+
.option('--since <date>', 'Start date for trend analysis (ISO format)')
|
|
174
|
+
.option('--until <date>', 'End date for trend analysis (ISO format)')
|
|
175
|
+
.action(async (period: string | undefined, options: { since?: string; until?: string }) => {
|
|
176
|
+
const resolvedPath = resolve('.')
|
|
177
|
+
process.stderr.write(`\nAnalyzing trend in ${resolvedPath}...\n`)
|
|
178
|
+
|
|
179
|
+
const config = await loadConfig(resolvedPath)
|
|
180
|
+
const analyzer = new TrendAnalyzer(resolvedPath, config)
|
|
181
|
+
|
|
182
|
+
const trendData = await analyzer.analyzeTrend({
|
|
183
|
+
period: period as 'week' | 'month' | 'quarter' | 'year',
|
|
184
|
+
since: options.since,
|
|
185
|
+
until: options.until
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
process.stderr.write(`\nTrend analysis complete:\n`)
|
|
189
|
+
process.stdout.write(JSON.stringify(trendData, null, 2) + '\n')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
program
|
|
193
|
+
.command('blame [target]')
|
|
194
|
+
.description('Analyze which files/rules contribute most to technical debt')
|
|
195
|
+
.option('--top <n>', 'Number of top contributors to show (default: 10)', '10')
|
|
196
|
+
.action(async (target: string | undefined, options: { top: string }) => {
|
|
197
|
+
const resolvedPath = resolve('.')
|
|
198
|
+
process.stderr.write(`\nAnalyzing blame in ${resolvedPath}...\n`)
|
|
199
|
+
|
|
200
|
+
const config = await loadConfig(resolvedPath)
|
|
201
|
+
const analyzer = new BlameAnalyzer(resolvedPath, config)
|
|
202
|
+
|
|
203
|
+
const blameData = await analyzer.analyzeBlame({
|
|
204
|
+
target: target as 'file' | 'rule' | 'overall' | undefined,
|
|
205
|
+
top: Number(options.top)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
process.stderr.write(`\nBlame analysis complete:\n`)
|
|
209
|
+
process.stdout.write(JSON.stringify(blameData, null, 2) + '\n')
|
|
210
|
+
})
|
|
211
|
+
|
|
166
212
|
program.parse()
|
package/src/printer.ts
CHANGED
|
@@ -118,66 +118,66 @@ function formatFixSuggestion(issue: DriftIssue): string[] {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export function printConsole(report: DriftReport, options?: { showFix?: boolean }): void {
|
|
121
|
-
const sep = kleur.gray(' ' + '─'.repeat(50))
|
|
122
|
-
|
|
123
|
-
console.log()
|
|
124
|
-
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'))
|
|
125
|
-
console.log(sep)
|
|
126
|
-
console.log()
|
|
127
|
-
|
|
128
|
-
const grade = scoreToGrade(report.totalScore)
|
|
129
|
-
const scoreColor = report.totalScore === 0
|
|
130
|
-
? kleur.green
|
|
131
|
-
: report.totalScore < 45
|
|
132
|
-
? kleur.yellow
|
|
133
|
-
: kleur.red
|
|
134
|
-
|
|
135
|
-
const bar = scoreBar(report.totalScore)
|
|
136
|
-
console.log(
|
|
137
|
-
` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
const cleanFiles = report.totalFiles - report.files.length
|
|
141
|
-
console.log(
|
|
142
|
-
kleur.gray(
|
|
143
|
-
` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
console.log()
|
|
147
|
-
|
|
148
|
-
// Top issues in header
|
|
149
|
-
const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
150
|
-
if (topRules.length > 0) {
|
|
151
|
-
const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`)
|
|
152
|
-
console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`)
|
|
153
|
-
console.log()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
console.log(sep)
|
|
157
|
-
console.log()
|
|
158
|
-
|
|
159
|
-
if (report.files.length === 0) {
|
|
160
|
-
console.log(kleur.green(' No drift detected. Clean codebase.'))
|
|
161
|
-
console.log()
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (const file of report.files) {
|
|
166
|
-
const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '')
|
|
167
|
-
console.log(
|
|
168
|
-
kleur.bold().white(` ${rel}`) +
|
|
169
|
-
kleur.gray(` (score ${file.score}/100)`)
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
for (const issue of file.issues) {
|
|
173
|
-
const icon = severityIcon(issue.severity)
|
|
174
|
-
const colorFn = (s: string) =>
|
|
175
|
-
issue.severity === 'error'
|
|
176
|
-
? kleur.red(s)
|
|
177
|
-
: issue.severity === 'warning'
|
|
178
|
-
? kleur.yellow(s)
|
|
179
|
-
: kleur.cyan(s)
|
|
180
|
-
|
|
121
|
+
const sep = kleur.gray(' ' + '─'.repeat(50))
|
|
122
|
+
|
|
123
|
+
console.log()
|
|
124
|
+
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'))
|
|
125
|
+
console.log(sep)
|
|
126
|
+
console.log()
|
|
127
|
+
|
|
128
|
+
const grade = scoreToGrade(report.totalScore)
|
|
129
|
+
const scoreColor = report.totalScore === 0
|
|
130
|
+
? kleur.green
|
|
131
|
+
: report.totalScore < 45
|
|
132
|
+
? kleur.yellow
|
|
133
|
+
: kleur.red
|
|
134
|
+
|
|
135
|
+
const bar = scoreBar(report.totalScore)
|
|
136
|
+
console.log(
|
|
137
|
+
` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const cleanFiles = report.totalFiles - report.files.length
|
|
141
|
+
console.log(
|
|
142
|
+
kleur.gray(
|
|
143
|
+
` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
console.log()
|
|
147
|
+
|
|
148
|
+
// Top issues in header
|
|
149
|
+
const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
|
150
|
+
if (topRules.length > 0) {
|
|
151
|
+
const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`)
|
|
152
|
+
console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`)
|
|
153
|
+
console.log()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(sep)
|
|
157
|
+
console.log()
|
|
158
|
+
|
|
159
|
+
if (report.files.length === 0) {
|
|
160
|
+
console.log(kleur.green(' No drift detected. Clean codebase.'))
|
|
161
|
+
console.log()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const file of report.files) {
|
|
166
|
+
const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '')
|
|
167
|
+
console.log(
|
|
168
|
+
kleur.bold().white(` ${rel}`) +
|
|
169
|
+
kleur.gray(` (score ${file.score}/100)`)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for (const issue of file.issues) {
|
|
173
|
+
const icon = severityIcon(issue.severity)
|
|
174
|
+
const colorFn = (s: string) =>
|
|
175
|
+
issue.severity === 'error'
|
|
176
|
+
? kleur.red(s)
|
|
177
|
+
: issue.severity === 'warning'
|
|
178
|
+
? kleur.yellow(s)
|
|
179
|
+
: kleur.cyan(s)
|
|
180
|
+
|
|
181
181
|
console.log(
|
|
182
182
|
` ${colorFn(icon)} ` +
|
|
183
183
|
kleur.gray(`L${issue.line}`) +
|
package/src/report.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
2
3
|
import { DriftReport, DriftIssue } from './types.js'
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const require = createRequire(import.meta.url)
|
|
6
|
+
const { version: VERSION } = require('../package.json') as { version: string }
|
|
5
7
|
|
|
6
8
|
function severityColor(severity: DriftIssue['severity']): string {
|
|
7
9
|
switch (severity) {
|
package/src/types.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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
|
-
|
|
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
16
|
export interface DriftReport {
|
|
17
17
|
scannedAt: string
|
|
18
18
|
targetPath: string
|
|
@@ -112,3 +112,47 @@ export interface DriftDiff {
|
|
|
112
112
|
newIssuesCount: number
|
|
113
113
|
resolvedIssuesCount: number
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/** Historical analysis data for a single commit */
|
|
117
|
+
export interface HistoricalAnalysis {
|
|
118
|
+
commitHash: string;
|
|
119
|
+
commitDate: Date;
|
|
120
|
+
author: string;
|
|
121
|
+
message: string;
|
|
122
|
+
files: FileReport[];
|
|
123
|
+
totalScore: number;
|
|
124
|
+
averageScore: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Trend data point for score evolution */
|
|
128
|
+
export interface TrendDataPoint {
|
|
129
|
+
date: Date;
|
|
130
|
+
score: number;
|
|
131
|
+
fileCount: number;
|
|
132
|
+
avgIssuesPerFile: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Blame attribution data */
|
|
136
|
+
export interface BlameAttribution {
|
|
137
|
+
author: string;
|
|
138
|
+
email: string;
|
|
139
|
+
commits: number;
|
|
140
|
+
linesChanged: number;
|
|
141
|
+
issuesIntroduced: number;
|
|
142
|
+
avgScoreImpact: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Extended DriftReport with historical context */
|
|
146
|
+
export interface DriftTrendReport extends DriftReport {
|
|
147
|
+
trend: TrendDataPoint[];
|
|
148
|
+
regression: {
|
|
149
|
+
slope: number;
|
|
150
|
+
intercept: number;
|
|
151
|
+
r2: number;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Extended DriftReport with blame data */
|
|
156
|
+
export interface DriftBlameReport extends DriftReport {
|
|
157
|
+
blame: BlameAttribution[];
|
|
158
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,35 +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
|
-
}
|
|
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
|
+
}
|