@eduardbar/drift 0.6.0 → 0.7.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 +10 -0
- package/assets/og.svg +105 -105
- package/dist/analyzer.d.ts +5 -1
- package/dist/analyzer.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/packages/eslint-plugin-drift/README.md +88 -0
- package/packages/eslint-plugin-drift/package-lock.json +1250 -0
- package/packages/eslint-plugin-drift/package.json +47 -0
- package/packages/eslint-plugin-drift/src/index.ts +170 -0
- package/packages/eslint-plugin-drift/tsconfig.json +17 -0
- package/src/analyzer.ts +1 -1
- package/src/index.ts +2 -2
- package/src/printer.ts +60 -60
- package/src/types.ts +15 -15
- package/src/utils.ts +35 -35
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-drift",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin that exposes drift's technical debt rules for ESLint 9 flat config",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"eslint",
|
|
17
|
+
"eslintplugin",
|
|
18
|
+
"eslint-plugin",
|
|
19
|
+
"drift",
|
|
20
|
+
"technical-debt",
|
|
21
|
+
"ai",
|
|
22
|
+
"typescript"
|
|
23
|
+
],
|
|
24
|
+
"author": "eduardbar",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://github.com/eduardbar/drift#readme",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/eduardbar/drift.git"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"eslint": ">=9.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@eduardbar/drift": "^0.7.0",
|
|
36
|
+
"ts-morph": "^27.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.0.0",
|
|
40
|
+
"eslint": "^9.0.0",
|
|
41
|
+
"typescript": "^5.9.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { analyzeFile } from '@eduardbar/drift'
|
|
2
|
+
import type { FileReport } from '@eduardbar/drift'
|
|
3
|
+
import { Project } from 'ts-morph'
|
|
4
|
+
import type { Rule } from 'eslint'
|
|
5
|
+
|
|
6
|
+
// Tipos auxiliares
|
|
7
|
+
type RuleType = 'problem' | 'suggestion' | 'layout'
|
|
8
|
+
|
|
9
|
+
// Mapeo rule → descripción legible
|
|
10
|
+
const RULE_DOCS: Record<string, { type: RuleType; description: string }> = {
|
|
11
|
+
'large-file': { type: 'problem', description: 'Files over 300 lines — AI dumps everything into one place' },
|
|
12
|
+
'large-function': { type: 'problem', description: 'Functions over 50 lines — AI avoids splitting logic' },
|
|
13
|
+
'duplicate-function-name': { type: 'problem', description: 'Near-identical function names — AI regenerates instead of reusing' },
|
|
14
|
+
'high-complexity': { type: 'problem', description: 'Cyclomatic complexity > 10 — AI generates correct code, not simple code' },
|
|
15
|
+
'circular-dependency': { type: 'problem', description: 'Circular import chains between modules' },
|
|
16
|
+
'layer-violation': { type: 'problem', description: 'Import from a prohibited architectural layer (requires drift.config.ts)' },
|
|
17
|
+
'debug-leftover': { type: 'suggestion', description: 'console.log, TODO, FIXME comments left in production code' },
|
|
18
|
+
'dead-code': { type: 'suggestion', description: 'Unused imports — AI imports more than it uses' },
|
|
19
|
+
'any-abuse': { type: 'suggestion', description: 'Explicit any type — AI defaults to any when it cannot infer' },
|
|
20
|
+
'catch-swallow': { type: 'suggestion', description: 'Empty catch blocks — AI makes code not throw' },
|
|
21
|
+
'comment-contradiction': { type: 'suggestion', description: 'Comments that restate what the code already says' },
|
|
22
|
+
'deep-nesting': { type: 'suggestion', description: 'Nesting depth > 3 — unreadable control flow' },
|
|
23
|
+
'too-many-params': { type: 'suggestion', description: 'Functions with more than 4 parameters' },
|
|
24
|
+
'high-coupling': { type: 'suggestion', description: 'Files importing from more than 10 modules' },
|
|
25
|
+
'promise-style-mix': { type: 'suggestion', description: 'async/await and .then() mixed in the same file' },
|
|
26
|
+
'unused-export': { type: 'suggestion', description: 'Named exports never imported anywhere in the project' },
|
|
27
|
+
'dead-file': { type: 'suggestion', description: 'Files never imported by any other file' },
|
|
28
|
+
'unused-dependency': { type: 'suggestion', description: 'Packages in package.json never imported in source code' },
|
|
29
|
+
'cross-boundary-import': { type: 'suggestion', description: 'Module imports from another module outside allowed boundaries' },
|
|
30
|
+
'hardcoded-config': { type: 'suggestion', description: 'Hardcoded URLs, IPs, or connection strings — AI skips env vars' },
|
|
31
|
+
'inconsistent-error-handling': { type: 'suggestion', description: 'Mixed try/catch and .catch() patterns in the same file' },
|
|
32
|
+
'unnecessary-abstraction': { type: 'suggestion', description: 'Single-method interfaces or abstract classes never reused' },
|
|
33
|
+
'naming-inconsistency': { type: 'suggestion', description: 'Mixed camelCase and snake_case in the same scope' },
|
|
34
|
+
'no-return-type': { type: 'suggestion', description: 'Missing explicit return types on functions' },
|
|
35
|
+
'magic-number': { type: 'suggestion', description: 'Numeric literals used directly in logic' },
|
|
36
|
+
'over-commented': { type: 'suggestion', description: 'Functions where comments exceed 40% of lines' },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ts-morph Project singleton — reutilizado para todos los archivos en un lint run
|
|
40
|
+
const project = new Project({
|
|
41
|
+
skipAddingFilesFromTsConfig: true,
|
|
42
|
+
compilerOptions: { allowJs: true },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Cache de FileReport por filename para no llamar analyzeFile 26 veces por archivo
|
|
46
|
+
const cache = new Map<string, FileReport>()
|
|
47
|
+
|
|
48
|
+
function getFileReport(filename: string): FileReport {
|
|
49
|
+
if (cache.has(filename)) return cache.get(filename)!
|
|
50
|
+
|
|
51
|
+
// Obtener o agregar el SourceFile al Project
|
|
52
|
+
let sourceFile = project.getSourceFile(filename)
|
|
53
|
+
if (!sourceFile) {
|
|
54
|
+
sourceFile = project.addSourceFileAtPath(filename)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const report = analyzeFile(sourceFile)
|
|
58
|
+
cache.set(filename, report)
|
|
59
|
+
|
|
60
|
+
// Evitar memory leak en watch mode — mantener máximo 100 entradas
|
|
61
|
+
if (cache.size > 100) {
|
|
62
|
+
const firstKey = cache.keys().next().value
|
|
63
|
+
if (firstKey !== undefined) cache.delete(firstKey)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return report
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Crea una regla ESLint para una regla drift específica
|
|
70
|
+
function createRule(ruleName: string): Rule.RuleModule {
|
|
71
|
+
const doc = RULE_DOCS[ruleName] ?? { type: 'suggestion' as RuleType, description: ruleName }
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
meta: {
|
|
75
|
+
type: doc.type,
|
|
76
|
+
docs: {
|
|
77
|
+
description: doc.description,
|
|
78
|
+
url: `https://github.com/eduardbar/drift#${ruleName}`,
|
|
79
|
+
},
|
|
80
|
+
schema: [],
|
|
81
|
+
messages: {
|
|
82
|
+
issue: '{{ message }}',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
create(context) {
|
|
86
|
+
return {
|
|
87
|
+
'Program:exit'() {
|
|
88
|
+
const filename = context.filename
|
|
89
|
+
if (!filename.endsWith('.ts') && !filename.endsWith('.tsx')) return
|
|
90
|
+
if (filename.includes('node_modules')) return
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const fileReport = getFileReport(filename)
|
|
94
|
+
for (const issue of fileReport.issues) {
|
|
95
|
+
if (issue.rule !== ruleName) continue
|
|
96
|
+
const col = issue.column > 0 ? issue.column - 1 : 0
|
|
97
|
+
context.report({
|
|
98
|
+
loc: {
|
|
99
|
+
start: { line: issue.line, column: col },
|
|
100
|
+
end: { line: issue.line, column: col + 1 },
|
|
101
|
+
},
|
|
102
|
+
messageId: 'issue',
|
|
103
|
+
data: { message: issue.message },
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Archivo no parseable por ts-morph — silenciar
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Objeto con todas las reglas
|
|
116
|
+
const rules: Record<string, Rule.RuleModule> = Object.fromEntries(
|
|
117
|
+
Object.keys(RULE_DOCS).map(name => [name, createRule(name)])
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Plugin object
|
|
121
|
+
const plugin = {
|
|
122
|
+
meta: {
|
|
123
|
+
name: 'eslint-plugin-drift',
|
|
124
|
+
version: '0.1.0',
|
|
125
|
+
},
|
|
126
|
+
rules,
|
|
127
|
+
configs: {} as Record<string, unknown>,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Config recommended — todas las reglas en su severidad canónica de drift
|
|
131
|
+
Object.assign(plugin.configs, {
|
|
132
|
+
recommended: [
|
|
133
|
+
{
|
|
134
|
+
plugins: { drift: plugin },
|
|
135
|
+
rules: {
|
|
136
|
+
// errors
|
|
137
|
+
'drift/large-file': 'error',
|
|
138
|
+
'drift/large-function': 'error',
|
|
139
|
+
'drift/duplicate-function-name': 'error',
|
|
140
|
+
'drift/high-complexity': 'error',
|
|
141
|
+
'drift/circular-dependency': 'error',
|
|
142
|
+
'drift/layer-violation': 'error',
|
|
143
|
+
// warnings
|
|
144
|
+
'drift/debug-leftover': 'warn',
|
|
145
|
+
'drift/dead-code': 'warn',
|
|
146
|
+
'drift/any-abuse': 'warn',
|
|
147
|
+
'drift/catch-swallow': 'warn',
|
|
148
|
+
'drift/comment-contradiction': 'warn',
|
|
149
|
+
'drift/deep-nesting': 'warn',
|
|
150
|
+
'drift/too-many-params': 'warn',
|
|
151
|
+
'drift/high-coupling': 'warn',
|
|
152
|
+
'drift/promise-style-mix': 'warn',
|
|
153
|
+
'drift/unused-export': 'warn',
|
|
154
|
+
'drift/dead-file': 'warn',
|
|
155
|
+
'drift/unused-dependency': 'warn',
|
|
156
|
+
'drift/cross-boundary-import': 'warn',
|
|
157
|
+
'drift/hardcoded-config': 'warn',
|
|
158
|
+
'drift/inconsistent-error-handling': 'warn',
|
|
159
|
+
'drift/unnecessary-abstraction': 'warn',
|
|
160
|
+
'drift/naming-inconsistency': 'warn',
|
|
161
|
+
// info → ESLint no tiene "info", mapeamos a "warn"
|
|
162
|
+
'drift/no-return-type': 'warn',
|
|
163
|
+
'drift/magic-number': 'warn',
|
|
164
|
+
'drift/over-commented': 'warn',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
export default plugin
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import type { DriftIssue, FileReport, DriftConfig, LayerDefinition, ModuleBoundary } from './types.js'
|
|
14
14
|
|
|
15
15
|
// Rules and their drift score weight
|
|
16
|
-
const RULE_WEIGHTS: Record<string, { severity: DriftIssue['severity']; weight: number }> = {
|
|
16
|
+
export const RULE_WEIGHTS: Record<string, { severity: DriftIssue['severity']; weight: number }> = {
|
|
17
17
|
'large-file': { severity: 'error', weight: 20 },
|
|
18
18
|
'large-function': { severity: 'error', weight: 15 },
|
|
19
19
|
'debug-leftover': { severity: 'warning', weight: 10 },
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { analyzeProject, analyzeFile } from './analyzer.js'
|
|
1
|
+
export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js'
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js'
|
|
3
3
|
export { computeDiff } from './diff.js'
|
|
4
|
-
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff } from './types.js'
|
|
4
|
+
export type { DriftReport, FileReport, DriftIssue, DriftDiff, FileDiff, DriftConfig } from './types.js'
|
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/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
|
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
|
+
}
|