@eduardbar/drift 0.9.1 → 1.1.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/.github/actions/drift-scan/README.md +61 -0
- package/.github/actions/drift-scan/action.yml +65 -0
- package/.github/workflows/publish-vscode.yml +78 -0
- package/AGENTS.md +83 -23
- package/README.md +69 -2
- package/ROADMAP.md +130 -98
- package/dist/analyzer.d.ts +8 -38
- package/dist/analyzer.js +181 -1526
- package/dist/badge.js +40 -22
- package/dist/ci.js +32 -18
- package/dist/cli.js +125 -4
- package/dist/config.js +1 -1
- package/dist/diff.d.ts +0 -7
- package/dist/diff.js +26 -25
- package/dist/fix.d.ts +17 -0
- package/dist/fix.js +132 -0
- package/dist/git/blame.d.ts +22 -0
- package/dist/git/blame.js +227 -0
- package/dist/git/helpers.d.ts +36 -0
- package/dist/git/helpers.js +152 -0
- package/dist/git/trend.d.ts +21 -0
- package/dist/git/trend.js +81 -0
- package/dist/git.d.ts +0 -13
- package/dist/git.js +27 -21
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/map.d.ts +3 -0
- package/dist/map.js +103 -0
- package/dist/metrics.d.ts +4 -0
- package/dist/metrics.js +176 -0
- package/dist/plugins.d.ts +6 -0
- package/dist/plugins.js +74 -0
- package/dist/printer.js +20 -0
- package/dist/report.js +654 -293
- package/dist/reporter.js +85 -2
- package/dist/review.d.ts +15 -0
- package/dist/review.js +80 -0
- package/dist/rules/comments.d.ts +4 -0
- package/dist/rules/comments.js +45 -0
- package/dist/rules/complexity.d.ts +4 -0
- package/dist/rules/complexity.js +51 -0
- package/dist/rules/coupling.d.ts +4 -0
- package/dist/rules/coupling.js +19 -0
- package/dist/rules/magic.d.ts +4 -0
- package/dist/rules/magic.js +33 -0
- package/dist/rules/nesting.d.ts +5 -0
- package/dist/rules/nesting.js +82 -0
- package/dist/rules/phase0-basic.d.ts +11 -0
- package/dist/rules/phase0-basic.js +183 -0
- package/dist/rules/phase1-complexity.d.ts +7 -0
- package/dist/rules/phase1-complexity.js +8 -0
- package/dist/rules/phase2-crossfile.d.ts +23 -0
- package/dist/rules/phase2-crossfile.js +135 -0
- package/dist/rules/phase3-arch.d.ts +23 -0
- package/dist/rules/phase3-arch.js +151 -0
- package/dist/rules/phase3-configurable.d.ts +6 -0
- package/dist/rules/phase3-configurable.js +97 -0
- package/dist/rules/phase5-ai.d.ts +8 -0
- package/dist/rules/phase5-ai.js +262 -0
- package/dist/rules/phase8-semantic.d.ts +17 -0
- package/dist/rules/phase8-semantic.js +110 -0
- package/dist/rules/promise.d.ts +4 -0
- package/dist/rules/promise.js +24 -0
- package/dist/rules/shared.d.ts +7 -0
- package/dist/rules/shared.js +27 -0
- package/dist/snapshot.d.ts +19 -0
- package/dist/snapshot.js +119 -0
- package/dist/types.d.ts +69 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +1 -0
- package/docs/AGENTS.md +146 -0
- package/docs/PRD.md +208 -0
- package/package.json +8 -3
- package/packages/eslint-plugin-drift/src/index.ts +1 -1
- package/packages/vscode-drift/.vscodeignore +9 -0
- package/packages/vscode-drift/LICENSE +21 -0
- package/packages/vscode-drift/README.md +64 -0
- package/packages/vscode-drift/images/icon.png +0 -0
- package/packages/vscode-drift/images/icon.svg +30 -0
- package/packages/vscode-drift/package-lock.json +485 -0
- package/packages/vscode-drift/package.json +119 -0
- package/packages/vscode-drift/src/analyzer.ts +40 -0
- package/packages/vscode-drift/src/diagnostics.ts +55 -0
- package/packages/vscode-drift/src/extension.ts +135 -0
- package/packages/vscode-drift/src/statusbar.ts +55 -0
- package/packages/vscode-drift/src/treeview.ts +110 -0
- package/packages/vscode-drift/tsconfig.json +18 -0
- package/packages/vscode-drift/vscode-drift-0.1.0.vsix +0 -0
- package/packages/vscode-drift/vscode-drift-0.1.1.vsix +0 -0
- package/src/analyzer.ts +248 -1765
- package/src/badge.ts +38 -16
- package/src/ci.ts +38 -17
- package/src/cli.ts +143 -4
- package/src/config.ts +1 -1
- package/src/diff.ts +36 -30
- package/src/fix.ts +178 -0
- package/src/git/blame.ts +279 -0
- package/src/git/helpers.ts +198 -0
- package/src/git/trend.ts +117 -0
- package/src/git.ts +33 -24
- package/src/index.ts +16 -1
- package/src/map.ts +117 -0
- package/src/metrics.ts +200 -0
- package/src/plugins.ts +76 -0
- package/src/printer.ts +20 -0
- package/src/report.ts +666 -296
- package/src/reporter.ts +95 -2
- package/src/review.ts +98 -0
- package/src/rules/comments.ts +56 -0
- package/src/rules/complexity.ts +57 -0
- package/src/rules/coupling.ts +23 -0
- package/src/rules/magic.ts +38 -0
- package/src/rules/nesting.ts +88 -0
- package/src/rules/phase0-basic.ts +194 -0
- package/src/rules/phase1-complexity.ts +8 -0
- package/src/rules/phase2-crossfile.ts +177 -0
- package/src/rules/phase3-arch.ts +183 -0
- package/src/rules/phase3-configurable.ts +132 -0
- package/src/rules/phase5-ai.ts +292 -0
- package/src/rules/phase8-semantic.ts +136 -0
- package/src/rules/promise.ts +29 -0
- package/src/rules/shared.ts +39 -0
- package/src/snapshot.ts +175 -0
- package/src/types.ts +75 -1
- package/src/utils.ts +3 -1
- package/tests/helpers.ts +45 -0
- package/tests/new-features.test.ts +153 -0
- package/tests/rules.test.ts +1269 -0
- package/vitest.config.ts +15 -0
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SourceFile } from 'ts-morph'
|
|
2
|
+
|
|
1
3
|
export interface DriftIssue {
|
|
2
4
|
rule: string
|
|
3
5
|
severity: 'error' | 'warning' | 'info'
|
|
@@ -26,6 +28,39 @@ export interface DriftReport {
|
|
|
26
28
|
infos: number
|
|
27
29
|
byRule: Record<string, number>
|
|
28
30
|
}
|
|
31
|
+
quality: RepoQualityScore
|
|
32
|
+
maintenanceRisk: MaintenanceRiskMetrics
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RepoQualityScore {
|
|
36
|
+
overall: number
|
|
37
|
+
dimensions: {
|
|
38
|
+
architecture: number
|
|
39
|
+
complexity: number
|
|
40
|
+
'ai-patterns': number
|
|
41
|
+
testing: number
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RiskHotspot {
|
|
46
|
+
file: string
|
|
47
|
+
driftScore: number
|
|
48
|
+
complexityIssues: number
|
|
49
|
+
hasNearbyTests: boolean
|
|
50
|
+
changeFrequency: number
|
|
51
|
+
risk: number
|
|
52
|
+
reasons: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MaintenanceRiskMetrics {
|
|
56
|
+
score: number
|
|
57
|
+
level: 'low' | 'medium' | 'high' | 'critical'
|
|
58
|
+
hotspots: RiskHotspot[]
|
|
59
|
+
signals: {
|
|
60
|
+
highComplexityFiles: number
|
|
61
|
+
filesWithoutNearbyTests: number
|
|
62
|
+
frequentChangeFiles: number
|
|
63
|
+
}
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
export interface AIOutput {
|
|
@@ -35,8 +70,13 @@ export interface AIOutput {
|
|
|
35
70
|
total_issues: number
|
|
36
71
|
files_affected: number
|
|
37
72
|
files_clean: number
|
|
73
|
+
ai_likelihood: number
|
|
74
|
+
ai_code_smell_score: number
|
|
38
75
|
}
|
|
76
|
+
files_suspected: Array<{ path: string; ai_likelihood: number; triggers: string[] }>
|
|
39
77
|
priority_order: AIIssue[]
|
|
78
|
+
maintenance_risk: MaintenanceRiskMetrics
|
|
79
|
+
quality: RepoQualityScore
|
|
40
80
|
context_for_ai: {
|
|
41
81
|
project_type: string
|
|
42
82
|
scan_path: string
|
|
@@ -86,6 +126,40 @@ export interface ModuleBoundary {
|
|
|
86
126
|
export interface DriftConfig {
|
|
87
127
|
layers?: LayerDefinition[]
|
|
88
128
|
modules?: ModuleBoundary[]
|
|
129
|
+
plugins?: string[]
|
|
130
|
+
architectureRules?: {
|
|
131
|
+
controllerNoDb?: boolean
|
|
132
|
+
serviceNoHttp?: boolean
|
|
133
|
+
maxFunctionLines?: number
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface PluginRuleContext {
|
|
138
|
+
projectRoot: string
|
|
139
|
+
filePath: string
|
|
140
|
+
config?: DriftConfig
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface DriftPluginRule {
|
|
144
|
+
name: string
|
|
145
|
+
severity?: DriftIssue['severity']
|
|
146
|
+
weight?: number
|
|
147
|
+
detect: (file: SourceFile, context: PluginRuleContext) => DriftIssue[]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DriftPlugin {
|
|
151
|
+
name: string
|
|
152
|
+
rules: DriftPluginRule[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface LoadedPlugin {
|
|
156
|
+
id: string
|
|
157
|
+
plugin: DriftPlugin
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface PluginLoadError {
|
|
161
|
+
pluginId: string
|
|
162
|
+
message: string
|
|
89
163
|
}
|
|
90
164
|
|
|
91
165
|
// ---------------------------------------------------------------------------
|
|
@@ -155,4 +229,4 @@ export interface DriftTrendReport extends DriftReport {
|
|
|
155
229
|
/** Extended DriftReport with blame data */
|
|
156
230
|
export interface DriftBlameReport extends DriftReport {
|
|
157
231
|
blame: BlameAttribution[];
|
|
158
|
-
}
|
|
232
|
+
}
|
package/src/utils.ts
CHANGED
package/tests/helpers.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// drift-ignore-file
|
|
2
|
+
import { Project } from 'ts-morph'
|
|
3
|
+
import { analyzeFile } from '../src/analyzer.js'
|
|
4
|
+
import type { DriftConfig } from '../src/types.js'
|
|
5
|
+
import type { FileReport } from '../src/types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Crea un SourceFile temporal en memoria y corre analyzeFile sobre él.
|
|
9
|
+
* El filePath por defecto es 'test.ts' (no en test/spec para que
|
|
10
|
+
* hardcoded-config NO lo skip automáticamente).
|
|
11
|
+
* Acepta un filename opcional para testear .js/.jsx/.tsx.
|
|
12
|
+
*/
|
|
13
|
+
export function analyzeCode(code: string, config?: Partial<DriftConfig>, filename = 'test.ts'): FileReport {
|
|
14
|
+
const project = new Project({
|
|
15
|
+
useInMemoryFileSystem: true,
|
|
16
|
+
compilerOptions: { allowJs: true, jsx: 1 }, // 1 = JsxEmit.Preserve
|
|
17
|
+
})
|
|
18
|
+
const sourceFile = project.createSourceFile(filename, code)
|
|
19
|
+
return analyzeFile(sourceFile, config as DriftConfig)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extrae solo los nombres de reglas que dispararon */
|
|
23
|
+
export function getRules(code: string, config?: Partial<DriftConfig>, filename = 'test.ts'): string[] {
|
|
24
|
+
return analyzeCode(code, config, filename).issues.map(i => i.rule)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Cuenta cuántas veces disparó una regla específica */
|
|
28
|
+
export function countRule(code: string, rule: string, filePath = 'test.ts'): number {
|
|
29
|
+
return analyzeCode(code, undefined, filePath).issues.filter(i => i.rule === rule).length
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Genera N líneas de código válido TypeScript (para large-file) */
|
|
33
|
+
export function generateLines(n: number): string {
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
for (let i = 0; i < n; i++) {
|
|
36
|
+
lines.push(`const _v${i} = ${i}`)
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Genera una función con N líneas de cuerpo (para large-function) */
|
|
42
|
+
export function generateFunction(bodyLines: number): string {
|
|
43
|
+
const body = Array.from({ length: bodyLines }, (_, i) => ` const _x${i} = ${i}`).join('\n')
|
|
44
|
+
return `function bigFn(): void {\n${body}\n}`
|
|
45
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { analyzeProject } from '../src/analyzer.js'
|
|
6
|
+
import { buildReport, formatAIOutput } from '../src/reporter.js'
|
|
7
|
+
import { formatReviewMarkdown, type DriftReview } from '../src/review.js'
|
|
8
|
+
import { generateArchitectureSvg } from '../src/map.js'
|
|
9
|
+
import { applyFixes } from '../src/fix.js'
|
|
10
|
+
|
|
11
|
+
describe('new feature MVP', () => {
|
|
12
|
+
let tmpDir = ''
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
|
|
16
|
+
tmpDir = ''
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('includes ai_likelihood and suspected files in --ai output', () => {
|
|
20
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'drift-ai-output-'))
|
|
21
|
+
writeFileSync(join(tmpDir, 'bad.ts'), [
|
|
22
|
+
'function x(a: any) {',
|
|
23
|
+
' // return value',
|
|
24
|
+
' if (a) return 1',
|
|
25
|
+
' return 0',
|
|
26
|
+
'}',
|
|
27
|
+
'const URL = "https://api.example.com"',
|
|
28
|
+
].join('\n'))
|
|
29
|
+
|
|
30
|
+
const files = analyzeProject(tmpDir)
|
|
31
|
+
const report = buildReport(tmpDir, files)
|
|
32
|
+
const ai = formatAIOutput(report)
|
|
33
|
+
|
|
34
|
+
expect(ai.summary.ai_likelihood).toBeGreaterThanOrEqual(0)
|
|
35
|
+
expect(typeof ai.summary.ai_code_smell_score).toBe('number')
|
|
36
|
+
expect(Array.isArray(ai.files_suspected)).toBe(true)
|
|
37
|
+
expect(ai.maintenance_risk).toBeDefined()
|
|
38
|
+
expect(ai.quality).toBeDefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('formats review markdown for PR comments', () => {
|
|
42
|
+
const review: DriftReview = {
|
|
43
|
+
baseRef: 'origin/main',
|
|
44
|
+
scannedAt: new Date().toISOString(),
|
|
45
|
+
totalDelta: 12,
|
|
46
|
+
newIssues: 3,
|
|
47
|
+
resolvedIssues: 1,
|
|
48
|
+
status: 'regressed',
|
|
49
|
+
summary: 'Drift regressed.',
|
|
50
|
+
markdown: '',
|
|
51
|
+
diff: {
|
|
52
|
+
baseRef: 'origin/main',
|
|
53
|
+
projectPath: '/tmp/repo',
|
|
54
|
+
scannedAt: new Date().toISOString(),
|
|
55
|
+
files: [{
|
|
56
|
+
path: 'src/a.ts',
|
|
57
|
+
scoreBefore: 10,
|
|
58
|
+
scoreAfter: 20,
|
|
59
|
+
scoreDelta: 10,
|
|
60
|
+
newIssues: [],
|
|
61
|
+
resolvedIssues: [],
|
|
62
|
+
}],
|
|
63
|
+
totalScoreBefore: 20,
|
|
64
|
+
totalScoreAfter: 32,
|
|
65
|
+
totalDelta: 12,
|
|
66
|
+
newIssuesCount: 3,
|
|
67
|
+
resolvedIssuesCount: 1,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const md = formatReviewMarkdown(review)
|
|
72
|
+
expect(md).toContain('## drift review')
|
|
73
|
+
expect(md).toContain('Base ref: `origin/main`')
|
|
74
|
+
expect(md).toContain('src/a.ts')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('detects configurable architecture rules', () => {
|
|
78
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'drift-arch-rules-'))
|
|
79
|
+
mkdirSync(join(tmpDir, 'controllers'))
|
|
80
|
+
mkdirSync(join(tmpDir, 'services'))
|
|
81
|
+
|
|
82
|
+
writeFileSync(join(tmpDir, 'controllers', 'user.controller.ts'), [
|
|
83
|
+
"import { prisma } from '../db/prisma.js'",
|
|
84
|
+
'export function getUser() { return prisma.user.findMany() }',
|
|
85
|
+
].join('\n'))
|
|
86
|
+
|
|
87
|
+
writeFileSync(join(tmpDir, 'services', 'mail.service.ts'), [
|
|
88
|
+
"import express from 'express'",
|
|
89
|
+
'export function sendMail() {',
|
|
90
|
+
' return express()',
|
|
91
|
+
'}',
|
|
92
|
+
].join('\n'))
|
|
93
|
+
|
|
94
|
+
writeFileSync(join(tmpDir, 'services', 'long.service.ts'), [
|
|
95
|
+
'export function veryLong() {',
|
|
96
|
+
...Array.from({ length: 25 }, (_, i) => ` const v${i} = ${i}`),
|
|
97
|
+
' return 1',
|
|
98
|
+
'}',
|
|
99
|
+
].join('\n'))
|
|
100
|
+
|
|
101
|
+
const reports = analyzeProject(tmpDir, {
|
|
102
|
+
architectureRules: {
|
|
103
|
+
controllerNoDb: true,
|
|
104
|
+
serviceNoHttp: true,
|
|
105
|
+
maxFunctionLines: 8,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const rules = reports.flatMap((report) => report.issues.map((issue) => issue.rule))
|
|
110
|
+
expect(rules).toContain('controller-no-db')
|
|
111
|
+
expect(rules).toContain('service-no-http')
|
|
112
|
+
expect(rules).toContain('max-function-lines')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('generates architecture SVG map', () => {
|
|
116
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'drift-map-'))
|
|
117
|
+
mkdirSync(join(tmpDir, 'api'))
|
|
118
|
+
mkdirSync(join(tmpDir, 'domain'))
|
|
119
|
+
writeFileSync(join(tmpDir, 'domain', 'user.ts'), 'export const x = 1\n')
|
|
120
|
+
writeFileSync(join(tmpDir, 'api', 'controller.ts'), "import { x } from '../domain/user.js'\nexport const y = x\n")
|
|
121
|
+
|
|
122
|
+
const svg = generateArchitectureSvg(tmpDir)
|
|
123
|
+
expect(svg.startsWith('<svg')).toBe(true)
|
|
124
|
+
expect(svg).toContain('api')
|
|
125
|
+
expect(svg).toContain('domain')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('falls back safely when plugin cannot be loaded', () => {
|
|
129
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'drift-plugin-fallback-'))
|
|
130
|
+
writeFileSync(join(tmpDir, 'index.ts'), 'export const x = 1\n')
|
|
131
|
+
|
|
132
|
+
const reports = analyzeProject(tmpDir, {
|
|
133
|
+
plugins: ['./does-not-exist-plugin.js'],
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const rules = reports.flatMap((report) => report.issues.map((issue) => issue.rule))
|
|
137
|
+
expect(rules).toContain('plugin-error')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('supports fix preview and write modes', async () => {
|
|
141
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'drift-fix-modes-'))
|
|
142
|
+
const file = join(tmpDir, 'sample.ts')
|
|
143
|
+
writeFileSync(file, "const x = 1\nconsole.log(x)\n")
|
|
144
|
+
|
|
145
|
+
const preview = await applyFixes(file, undefined, { preview: true })
|
|
146
|
+
expect(preview.length).toBeGreaterThan(0)
|
|
147
|
+
expect(readFileSync(file, 'utf8')).toContain('console.log')
|
|
148
|
+
|
|
149
|
+
const write = await applyFixes(file, undefined, { write: true })
|
|
150
|
+
expect(write.length).toBeGreaterThan(0)
|
|
151
|
+
expect(readFileSync(file, 'utf8')).not.toContain('console.log')
|
|
152
|
+
})
|
|
153
|
+
})
|