@eduardbar/drift 0.5.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 +21 -0
- package/README.md +48 -2
- package/assets/og.svg +105 -105
- package/dist/analyzer.d.ts +5 -1
- package/dist/analyzer.js +1 -1
- package/dist/badge.d.ts +2 -0
- package/dist/badge.js +57 -0
- package/dist/ci.d.ts +4 -0
- package/dist/ci.js +85 -0
- package/dist/cli.js +52 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/report.d.ts +3 -0
- package/dist/report.js +494 -0
- 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/badge.ts +60 -0
- package/src/ci.ts +87 -0
- package/src/cli.ts +55 -1
- package/src/index.ts +2 -2
- package/src/printer.ts +60 -60
- package/src/report.ts +500 -0
- package/src/types.ts +15 -15
- package/src/utils.ts +35 -35
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,27 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
11
11
|
|
|
12
12
|
### Added
|
|
13
13
|
|
|
14
|
+
- **Phase 6: Static HTML report + README badge** — output commands for visibility
|
|
15
|
+
- `drift report [path]` — generates a self-contained `drift-report.html` with score, per-file breakdown, collapsible issue list, and fix suggestions
|
|
16
|
+
- `drift badge [path]` — generates a `badge.svg` with the current drift score for your README
|
|
17
|
+
- `drift ci [path]` — emits GitHub Actions workflow annotations inline on PR diffs and writes a step summary; supports `--min-score` to gate PRs
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2026-02-24
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `eslint-plugin-drift` — separate npm package exposing all 26 drift rules as ESLint 9 flat config rules
|
|
25
|
+
- Each rule wraps drift's AST engine via `analyzeFile()` with a shared ts-morph `Project` instance
|
|
26
|
+
- Per-file result cache (max 100 entries) to prevent redundant analysis in watch mode
|
|
27
|
+
- `recommended` config array enabling all 26 rules at their canonical drift severity
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [0.5.0] — 2026-02-24
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
14
35
|
- **Phase 5: AI authorship heuristics** — 5 new rules that detect patterns AI code generators produce
|
|
15
36
|
- `over-commented` (info, weight 4): functions where comment density ≥ 40% — AI over-documents the obvious
|
|
16
37
|
- `hardcoded-config` (warning, weight 10): hardcoded URLs, IPs, or connection strings instead of env vars
|
package/README.md
CHANGED
|
@@ -105,6 +105,40 @@ drift diff --json # Output raw JSON diff
|
|
|
105
105
|
|
|
106
106
|
Shows score delta, new issues introduced, and issues resolved per file.
|
|
107
107
|
|
|
108
|
+
### `drift report [path]`
|
|
109
|
+
|
|
110
|
+
Generate a self-contained `drift-report.html` — open in any browser:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
drift report # scan current directory
|
|
114
|
+
drift report ./src # scan specific path
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
No server needed. The file embeds all styles and data inline.
|
|
118
|
+
|
|
119
|
+
### `drift badge [path]`
|
|
120
|
+
|
|
121
|
+
Generate a `badge.svg` with the current score for your README:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
drift badge # writes badge.svg to current directory
|
|
125
|
+
drift badge ./src # scan specific path
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Drop the generated file in your repo and reference it as a local badge.
|
|
129
|
+
|
|
130
|
+
### `drift ci [path]`
|
|
131
|
+
|
|
132
|
+
Emit GitHub Actions annotations and step summary:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
drift ci # scan current directory
|
|
136
|
+
drift ci ./src # scan specific path
|
|
137
|
+
drift ci --min-score 60 # exit code 1 if score exceeds threshold
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Outputs inline annotations visible in the PR diff. Use `--min-score` to gate merges.
|
|
141
|
+
|
|
108
142
|
### AI Integration
|
|
109
143
|
|
|
110
144
|
Use `--ai` to get structured output that LLMs can consume:
|
|
@@ -183,9 +217,21 @@ Without a config file, these two rules are silently skipped. All other rules run
|
|
|
183
217
|
|
|
184
218
|
---
|
|
185
219
|
|
|
186
|
-
## ⚙️ CI
|
|
220
|
+
## ⚙️ CI / GitHub Actions
|
|
221
|
+
|
|
222
|
+
Add drift to your PR workflow to gate on score and get inline annotations:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
- name: Run drift
|
|
226
|
+
run: npx @eduardbar/drift ci --min-score 60
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This will:
|
|
230
|
+
- Emit inline annotations on the exact lines with issues (visible in the PR diff)
|
|
231
|
+
- Write a summary to the GitHub Actions step summary
|
|
232
|
+
- Exit with code 1 if the score exceeds the threshold
|
|
187
233
|
|
|
188
|
-
|
|
234
|
+
If you only need a pass/fail gate without annotations, `scan` works too:
|
|
189
235
|
|
|
190
236
|
```yaml
|
|
191
237
|
- name: Check for vibe coding drift
|
package/assets/og.svg
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
-
<stop offset="0%" style="stop-color:#0a0a0f"/>
|
|
5
|
-
<stop offset="100%" style="stop-color:#0f0f1a"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
8
|
-
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
9
|
-
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
12
|
-
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
|
|
13
|
-
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
|
|
14
|
-
</linearGradient>
|
|
15
|
-
<filter id="blur-glow">
|
|
16
|
-
<feGaussianBlur stdDeviation="40" result="blur"/>
|
|
17
|
-
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
18
|
-
</filter>
|
|
19
|
-
<filter id="soft-glow">
|
|
20
|
-
<feGaussianBlur stdDeviation="3" result="blur"/>
|
|
21
|
-
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
22
|
-
</filter>
|
|
23
|
-
</defs>
|
|
24
|
-
|
|
25
|
-
<!-- Background -->
|
|
26
|
-
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
27
|
-
|
|
28
|
-
<!-- Ambient glow top-left -->
|
|
29
|
-
<ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
|
|
30
|
-
|
|
31
|
-
<!-- Ambient glow bottom-right -->
|
|
32
|
-
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
|
|
33
|
-
|
|
34
|
-
<!-- Grid lines subtle -->
|
|
35
|
-
<line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
36
|
-
<line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
37
|
-
<line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
38
|
-
<line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
39
|
-
|
|
40
|
-
<!-- Left accent bar -->
|
|
41
|
-
<rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
|
|
42
|
-
|
|
43
|
-
<!-- Main title "drift" -->
|
|
44
|
-
<text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
|
|
45
|
-
|
|
46
|
-
<!-- Subtitle -->
|
|
47
|
-
<text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
|
|
48
|
-
|
|
49
|
-
<!-- Terminal block right side -->
|
|
50
|
-
<rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
51
|
-
|
|
52
|
-
<!-- Terminal header bar -->
|
|
53
|
-
<rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
|
|
54
|
-
<rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
|
|
55
|
-
|
|
56
|
-
<!-- Terminal dots -->
|
|
57
|
-
<circle cx="710" cy="185" r="6" fill="#ff5f57"/>
|
|
58
|
-
<circle cx="730" cy="185" r="6" fill="#febc2e"/>
|
|
59
|
-
<circle cx="750" cy="185" r="6" fill="#28c840"/>
|
|
60
|
-
|
|
61
|
-
<!-- Terminal text -->
|
|
62
|
-
<text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
|
|
63
|
-
<text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
|
|
64
|
-
|
|
65
|
-
<text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
|
|
66
|
-
|
|
67
|
-
<text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
|
|
68
|
-
<text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
|
|
69
|
-
<text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
|
|
70
|
-
|
|
71
|
-
<text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
|
|
72
|
-
|
|
73
|
-
<text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
|
|
74
|
-
<text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
|
|
75
|
-
<text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
|
|
76
|
-
<text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
|
|
77
|
-
|
|
78
|
-
<text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
79
|
-
<text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
|
|
80
|
-
<text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
|
|
81
|
-
|
|
82
|
-
<text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
83
|
-
<text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
|
|
84
|
-
<text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
|
|
85
|
-
|
|
86
|
-
<text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
|
|
87
|
-
<text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
|
|
88
|
-
<text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
|
|
89
|
-
|
|
90
|
-
<!-- Badges row -->
|
|
91
|
-
<rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
92
|
-
<text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
|
|
93
|
-
|
|
94
|
-
<rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
95
|
-
<text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
|
|
96
|
-
|
|
97
|
-
<rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
98
|
-
<text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
|
|
99
|
-
|
|
100
|
-
<!-- Author -->
|
|
101
|
-
<text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
|
|
102
|
-
|
|
103
|
-
<!-- Bottom accent line -->
|
|
104
|
-
<rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
|
|
105
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#0a0a0f"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#0f0f1a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
8
|
+
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
9
|
+
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
12
|
+
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
|
|
13
|
+
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<filter id="blur-glow">
|
|
16
|
+
<feGaussianBlur stdDeviation="40" result="blur"/>
|
|
17
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
18
|
+
</filter>
|
|
19
|
+
<filter id="soft-glow">
|
|
20
|
+
<feGaussianBlur stdDeviation="3" result="blur"/>
|
|
21
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
22
|
+
</filter>
|
|
23
|
+
</defs>
|
|
24
|
+
|
|
25
|
+
<!-- Background -->
|
|
26
|
+
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
27
|
+
|
|
28
|
+
<!-- Ambient glow top-left -->
|
|
29
|
+
<ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
|
|
30
|
+
|
|
31
|
+
<!-- Ambient glow bottom-right -->
|
|
32
|
+
<ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
|
|
33
|
+
|
|
34
|
+
<!-- Grid lines subtle -->
|
|
35
|
+
<line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
36
|
+
<line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
37
|
+
<line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
38
|
+
<line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
|
|
39
|
+
|
|
40
|
+
<!-- Left accent bar -->
|
|
41
|
+
<rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
|
|
42
|
+
|
|
43
|
+
<!-- Main title "drift" -->
|
|
44
|
+
<text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
|
|
45
|
+
|
|
46
|
+
<!-- Subtitle -->
|
|
47
|
+
<text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
|
|
48
|
+
|
|
49
|
+
<!-- Terminal block right side -->
|
|
50
|
+
<rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
|
|
51
|
+
|
|
52
|
+
<!-- Terminal header bar -->
|
|
53
|
+
<rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
|
|
54
|
+
<rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
|
|
55
|
+
|
|
56
|
+
<!-- Terminal dots -->
|
|
57
|
+
<circle cx="710" cy="185" r="6" fill="#ff5f57"/>
|
|
58
|
+
<circle cx="730" cy="185" r="6" fill="#febc2e"/>
|
|
59
|
+
<circle cx="750" cy="185" r="6" fill="#28c840"/>
|
|
60
|
+
|
|
61
|
+
<!-- Terminal text -->
|
|
62
|
+
<text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
|
|
63
|
+
<text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
|
|
64
|
+
|
|
65
|
+
<text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
|
|
66
|
+
|
|
67
|
+
<text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
|
|
68
|
+
<text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
|
|
69
|
+
<text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
|
|
70
|
+
|
|
71
|
+
<text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
|
|
72
|
+
|
|
73
|
+
<text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
|
|
74
|
+
<text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
|
|
75
|
+
<text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
|
|
76
|
+
<text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
|
|
77
|
+
|
|
78
|
+
<text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
79
|
+
<text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
|
|
80
|
+
<text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
|
|
81
|
+
|
|
82
|
+
<text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
|
|
83
|
+
<text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
|
|
84
|
+
<text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
|
|
85
|
+
|
|
86
|
+
<text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
|
|
87
|
+
<text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
|
|
88
|
+
<text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
|
|
89
|
+
|
|
90
|
+
<!-- Badges row -->
|
|
91
|
+
<rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
92
|
+
<text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
|
|
93
|
+
|
|
94
|
+
<rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
95
|
+
<text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
|
|
96
|
+
|
|
97
|
+
<rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
|
|
98
|
+
<text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
|
|
99
|
+
|
|
100
|
+
<!-- Author -->
|
|
101
|
+
<text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
|
|
102
|
+
|
|
103
|
+
<!-- Bottom accent line -->
|
|
104
|
+
<rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
|
|
105
|
+
</svg>
|
package/dist/analyzer.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { SourceFile } from 'ts-morph';
|
|
2
|
-
import type { FileReport, DriftConfig } from './types.js';
|
|
2
|
+
import type { DriftIssue, FileReport, DriftConfig } from './types.js';
|
|
3
|
+
export declare const RULE_WEIGHTS: Record<string, {
|
|
4
|
+
severity: DriftIssue['severity'];
|
|
5
|
+
weight: number;
|
|
6
|
+
}>;
|
|
3
7
|
export declare function analyzeFile(file: SourceFile): FileReport;
|
|
4
8
|
export declare function analyzeProject(targetPath: string, config?: DriftConfig): FileReport[];
|
|
5
9
|
//# sourceMappingURL=analyzer.d.ts.map
|
package/dist/analyzer.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { Project, SyntaxKind, } from 'ts-morph';
|
|
4
4
|
// Rules and their drift score weight
|
|
5
|
-
const RULE_WEIGHTS = {
|
|
5
|
+
export const RULE_WEIGHTS = {
|
|
6
6
|
'large-file': { severity: 'error', weight: 20 },
|
|
7
7
|
'large-function': { severity: 'error', weight: 15 },
|
|
8
8
|
'debug-leftover': { severity: 'warning', weight: 10 },
|
package/dist/badge.d.ts
ADDED
package/dist/badge.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const LEFT_WIDTH = 47;
|
|
2
|
+
const CHAR_WIDTH = 7;
|
|
3
|
+
const PADDING = 16;
|
|
4
|
+
function scoreColor(score) {
|
|
5
|
+
if (score < 20)
|
|
6
|
+
return '#4c1';
|
|
7
|
+
if (score < 45)
|
|
8
|
+
return '#dfb317';
|
|
9
|
+
if (score < 70)
|
|
10
|
+
return '#fe7d37';
|
|
11
|
+
return '#e05d44';
|
|
12
|
+
}
|
|
13
|
+
function scoreLabel(score) {
|
|
14
|
+
if (score < 20)
|
|
15
|
+
return 'LOW';
|
|
16
|
+
if (score < 45)
|
|
17
|
+
return 'MODERATE';
|
|
18
|
+
if (score < 70)
|
|
19
|
+
return 'HIGH';
|
|
20
|
+
return 'CRITICAL';
|
|
21
|
+
}
|
|
22
|
+
function rightWidth(text) {
|
|
23
|
+
return text.length * CHAR_WIDTH + PADDING;
|
|
24
|
+
}
|
|
25
|
+
export function generateBadge(score) {
|
|
26
|
+
const valueText = `${score} ${scoreLabel(score)}`;
|
|
27
|
+
const color = scoreColor(score);
|
|
28
|
+
const rWidth = rightWidth(valueText);
|
|
29
|
+
const totalWidth = LEFT_WIDTH + rWidth;
|
|
30
|
+
const leftCenterX = LEFT_WIDTH / 2;
|
|
31
|
+
const rightCenterX = LEFT_WIDTH + rWidth / 2;
|
|
32
|
+
// shields.io pattern: font-size="110" + scale(.1) = effective 11px
|
|
33
|
+
// all X/Y coords are ×10
|
|
34
|
+
const leftTextWidth = (LEFT_WIDTH - 10) * 10;
|
|
35
|
+
const rightTextWidth = (rWidth - PADDING) * 10;
|
|
36
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="20">
|
|
37
|
+
<linearGradient id="s" x2="0" y2="100%">
|
|
38
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
39
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
40
|
+
</linearGradient>
|
|
41
|
+
<clipPath id="r">
|
|
42
|
+
<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
|
|
43
|
+
</clipPath>
|
|
44
|
+
<g clip-path="url(#r)">
|
|
45
|
+
<rect width="${LEFT_WIDTH}" height="20" fill="#555"/>
|
|
46
|
+
<rect x="${LEFT_WIDTH}" width="${rWidth}" height="20" fill="${color}"/>
|
|
47
|
+
<rect width="${totalWidth}" height="20" fill="url(#s)"/>
|
|
48
|
+
</g>
|
|
49
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
|
|
50
|
+
<text x="${leftCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
|
|
51
|
+
<text x="${leftCenterX * 10}" y="140" transform="scale(.1)" textLength="${leftTextWidth}" lengthAdjust="spacing">drift</text>
|
|
52
|
+
<text x="${rightCenterX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
|
|
53
|
+
<text x="${rightCenterX * 10}" y="140" transform="scale(.1)" textLength="${rightTextWidth}" lengthAdjust="spacing">${valueText}</text>
|
|
54
|
+
</g>
|
|
55
|
+
</svg>`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=badge.js.map
|
package/dist/ci.d.ts
ADDED
package/dist/ci.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
function encodeMessage(msg) {
|
|
4
|
+
return msg
|
|
5
|
+
.replace(/%/g, '%25')
|
|
6
|
+
.replace(/\r/g, '%0D')
|
|
7
|
+
.replace(/\n/g, '%0A')
|
|
8
|
+
.replace(/:/g, '%3A')
|
|
9
|
+
.replace(/,/g, '%2C');
|
|
10
|
+
}
|
|
11
|
+
function severityToAnnotation(s) {
|
|
12
|
+
if (s === 'error')
|
|
13
|
+
return 'error';
|
|
14
|
+
if (s === 'warning')
|
|
15
|
+
return 'warning';
|
|
16
|
+
return 'notice';
|
|
17
|
+
}
|
|
18
|
+
function scoreLabel(score) {
|
|
19
|
+
if (score >= 80)
|
|
20
|
+
return 'A';
|
|
21
|
+
if (score >= 60)
|
|
22
|
+
return 'B';
|
|
23
|
+
if (score >= 40)
|
|
24
|
+
return 'C';
|
|
25
|
+
if (score >= 20)
|
|
26
|
+
return 'D';
|
|
27
|
+
return 'F';
|
|
28
|
+
}
|
|
29
|
+
export function emitCIAnnotations(report) {
|
|
30
|
+
for (const file of report.files) {
|
|
31
|
+
for (const issue of file.issues) {
|
|
32
|
+
const level = severityToAnnotation(issue.severity);
|
|
33
|
+
const relPath = relative(process.cwd(), file.path).replace(/\\/g, '/');
|
|
34
|
+
const msg = encodeMessage(`[drift/${issue.rule}] ${issue.message}`);
|
|
35
|
+
const line = issue.line ?? 1;
|
|
36
|
+
const col = issue.column ?? 1;
|
|
37
|
+
process.stdout.write(`::${level} file=${relPath},line=${line},col=${col}::${msg}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function printCISummary(report) {
|
|
42
|
+
const summaryPath = process.env['GITHUB_STEP_SUMMARY'];
|
|
43
|
+
if (!summaryPath)
|
|
44
|
+
return;
|
|
45
|
+
const score = report.totalScore;
|
|
46
|
+
const grade = scoreLabel(score);
|
|
47
|
+
let errors = 0;
|
|
48
|
+
let warnings = 0;
|
|
49
|
+
let info = 0;
|
|
50
|
+
for (const file of report.files) {
|
|
51
|
+
for (const issue of file.issues) {
|
|
52
|
+
if (issue.severity === 'error')
|
|
53
|
+
errors++;
|
|
54
|
+
else if (issue.severity === 'warning')
|
|
55
|
+
warnings++;
|
|
56
|
+
else
|
|
57
|
+
info++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const sorted = [...report.files]
|
|
61
|
+
.sort((a, b) => b.issues.length - a.issues.length)
|
|
62
|
+
.slice(0, 10);
|
|
63
|
+
const rows = sorted
|
|
64
|
+
.map((f) => {
|
|
65
|
+
const relPath = relative(process.cwd(), f.path).replace(/\\/g, '/');
|
|
66
|
+
return `| ${relPath} | ${f.score} | ${f.issues.length} |`;
|
|
67
|
+
})
|
|
68
|
+
.join('\n');
|
|
69
|
+
const md = [
|
|
70
|
+
'## drift scan results',
|
|
71
|
+
'',
|
|
72
|
+
`**Score:** ${score}/100 — Grade **${grade}**`,
|
|
73
|
+
'',
|
|
74
|
+
'### Top files by issue count',
|
|
75
|
+
'',
|
|
76
|
+
'| File | Score | Issues |',
|
|
77
|
+
'|------|-------|--------|',
|
|
78
|
+
rows,
|
|
79
|
+
'',
|
|
80
|
+
`**Total issues:** ${errors} errors, ${warnings} warnings, ${info} info`,
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
writeFileSync(summaryPath, md, { flag: 'a' });
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ci.js.map
|
package/dist/cli.js
CHANGED
|
@@ -8,11 +8,14 @@ import { printConsole, printDiff } from './printer.js';
|
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { extractFilesAtRef, cleanupTempDir } from './git.js';
|
|
10
10
|
import { computeDiff } from './diff.js';
|
|
11
|
+
import { generateHtmlReport } from './report.js';
|
|
12
|
+
import { generateBadge } from './badge.js';
|
|
13
|
+
import { emitCIAnnotations, printCISummary } from './ci.js';
|
|
11
14
|
const program = new Command();
|
|
12
15
|
program
|
|
13
16
|
.name('drift')
|
|
14
17
|
.description('Detect silent technical debt left by AI-generated code')
|
|
15
|
-
.version('0.
|
|
18
|
+
.version('0.6.0');
|
|
16
19
|
program
|
|
17
20
|
.command('scan [path]', { isDefault: true })
|
|
18
21
|
.description('Scan a directory for vibe coding drift')
|
|
@@ -95,5 +98,53 @@ program
|
|
|
95
98
|
cleanupTempDir(tempDir);
|
|
96
99
|
}
|
|
97
100
|
});
|
|
101
|
+
program
|
|
102
|
+
.command('report [path]')
|
|
103
|
+
.description('Generate a self-contained HTML report')
|
|
104
|
+
.option('-o, --output <file>', 'Output file path (default: drift-report.html)', 'drift-report.html')
|
|
105
|
+
.action(async (targetPath, options) => {
|
|
106
|
+
const resolvedPath = resolve(targetPath ?? '.');
|
|
107
|
+
process.stderr.write(`\nScanning ${resolvedPath}...\n`);
|
|
108
|
+
const config = await loadConfig(resolvedPath);
|
|
109
|
+
const files = analyzeProject(resolvedPath, config);
|
|
110
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
|
|
111
|
+
const report = buildReport(resolvedPath, files);
|
|
112
|
+
const html = generateHtmlReport(report);
|
|
113
|
+
const outPath = resolve(options.output);
|
|
114
|
+
writeFileSync(outPath, html, 'utf8');
|
|
115
|
+
process.stderr.write(` Report saved to ${outPath}\n\n`);
|
|
116
|
+
});
|
|
117
|
+
program
|
|
118
|
+
.command('badge [path]')
|
|
119
|
+
.description('Generate a badge.svg with the current drift score')
|
|
120
|
+
.option('-o, --output <file>', 'Output file path (default: badge.svg)', 'badge.svg')
|
|
121
|
+
.action(async (targetPath, options) => {
|
|
122
|
+
const resolvedPath = resolve(targetPath ?? '.');
|
|
123
|
+
process.stderr.write(`\nScanning ${resolvedPath}...\n`);
|
|
124
|
+
const config = await loadConfig(resolvedPath);
|
|
125
|
+
const files = analyzeProject(resolvedPath, config);
|
|
126
|
+
const report = buildReport(resolvedPath, files);
|
|
127
|
+
const svg = generateBadge(report.totalScore);
|
|
128
|
+
const outPath = resolve(options.output);
|
|
129
|
+
writeFileSync(outPath, svg, 'utf8');
|
|
130
|
+
process.stderr.write(` Badge saved to ${outPath}\n`);
|
|
131
|
+
process.stderr.write(` Score: ${report.totalScore}/100\n\n`);
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command('ci [path]')
|
|
135
|
+
.description('Emit GitHub Actions annotations and step summary')
|
|
136
|
+
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
137
|
+
.action(async (targetPath, options) => {
|
|
138
|
+
const resolvedPath = resolve(targetPath ?? '.');
|
|
139
|
+
const config = await loadConfig(resolvedPath);
|
|
140
|
+
const files = analyzeProject(resolvedPath, config);
|
|
141
|
+
const report = buildReport(resolvedPath, files);
|
|
142
|
+
emitCIAnnotations(report);
|
|
143
|
+
printCISummary(report);
|
|
144
|
+
const minScore = Number(options.minScore);
|
|
145
|
+
if (minScore > 0 && report.totalScore > minScore) {
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
98
149
|
program.parse();
|
|
99
150
|
//# sourceMappingURL=cli.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
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
4
|
//# sourceMappingURL=index.js.map
|
package/dist/report.d.ts
ADDED