@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 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 Integration
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
- Drop this into your GitHub Actions workflow to block merges when drift exceeds your threshold:
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>
@@ -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 },
@@ -0,0 +1,2 @@
1
+ export declare function generateBadge(score: number): string;
2
+ //# sourceMappingURL=badge.d.ts.map
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
@@ -0,0 +1,4 @@
1
+ import type { DriftReport } from './types.js';
2
+ export declare function emitCIAnnotations(report: DriftReport): void;
3
+ export declare function printCISummary(report: DriftReport): void;
4
+ //# sourceMappingURL=ci.d.ts.map
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.1.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
@@ -0,0 +1,3 @@
1
+ import { DriftReport } from './types.js';
2
+ export declare function generateHtmlReport(report: DriftReport): string;
3
+ //# sourceMappingURL=report.d.ts.map