@eduardbar/drift 0.1.0 → 0.2.1

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.
@@ -0,0 +1,58 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ push:
7
+ tags:
8
+ - 'v*'
9
+ workflow_dispatch:
10
+ inputs:
11
+ tag:
12
+ description: 'Tag version to publish (e.g., 0.2.0)'
13
+ required: true
14
+ type: string
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ publish:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v4
25
+ with:
26
+ ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.tag) || '' }}
27
+
28
+ - name: Setup Node.js
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: '20'
32
+ registry-url: 'https://registry.npmjs.org'
33
+ cache: 'npm'
34
+
35
+ - name: Verify version matches tag
36
+ run: |
37
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
38
+ TAG_VERSION="${{ inputs.tag }}"
39
+ else
40
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
41
+ fi
42
+ PKG_VERSION=$(node -p "require('./package.json').version")
43
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
44
+ echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
45
+ exit 1
46
+ fi
47
+ echo "Version check passed: $PKG_VERSION"
48
+
49
+ - name: Install dependencies
50
+ run: npm ci
51
+
52
+ - name: Build
53
+ run: npm run build
54
+
55
+ - name: Publish to npm
56
+ run: npm publish --access public
57
+ env:
58
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -25,10 +25,15 @@ drift scans your TypeScript/JavaScript codebase for the specific patterns AI too
25
25
  ```bash
26
26
  $ npx @eduardbar/drift scan ./src
27
27
 
28
- drift vibe coding debt detector
28
+ drift vibe coding debt detector
29
+ ──────────────────────────────────────────────────
29
30
 
30
- Score 67/100 HIGH
31
- 4 file(s) with issues · 5 errors · 12 warnings · 3 info
31
+ Score █████████████░░░░░░░ 67/100 HIGH
32
+ 4 file(s) with issues · 5 errors · 12 warnings · 3 info · 18 files clean
33
+
34
+ Top issues: debug-leftover ×8 · any-abuse ×5 · no-return-type ×3
35
+
36
+ ──────────────────────────────────────────────────
32
37
 
33
38
  src/api/users.ts (score 85/100)
34
39
  ✖ L1 large-file File has 412 lines (threshold: 300)
@@ -39,11 +44,6 @@ $ npx @eduardbar/drift scan ./src
39
44
  src/utils/helpers.ts (score 70/100)
40
45
  ✖ L12 duplicate-function-name 'formatDate' looks like a duplicate
41
46
  ▲ L55 dead-code Unused import 'debounce'
42
-
43
- Top rules:
44
- · debug-leftover: 8
45
- · any-abuse: 5
46
- · no-return-type: 3
47
47
  ```
48
48
 
49
49
  ---
@@ -67,11 +67,16 @@ npm install --save-dev @eduardbar/drift
67
67
  ## 🚀 Usage
68
68
 
69
69
  ```bash
70
- drift scan # Scan current directory
71
- drift scan ./src # Scan a specific path
72
- drift scan ./src --output report.md # Write Markdown report to file
73
- drift scan ./src --json # Output raw JSON
74
- drift scan ./src --min-score 50 # Exit code 1 if score > 50
70
+ # Recommended no install needed
71
+ npx @eduardbar/drift scan .
72
+ npx @eduardbar/drift scan ./src
73
+ npx @eduardbar/drift scan ./src --output report.md
74
+ npx @eduardbar/drift scan ./src --json
75
+ npx @eduardbar/drift scan ./src --min-score 50
76
+
77
+ # Install globally if you want the short 'drift' command
78
+ npm install -g @eduardbar/drift
79
+ drift scan .
75
80
  ```
76
81
 
77
82
  ### Options
@@ -150,6 +155,12 @@ npm run build
150
155
  node dist/cli.js scan ./src
151
156
  ```
152
157
 
158
+ Or without cloning:
159
+
160
+ ```bash
161
+ npx @eduardbar/drift scan .
162
+ ```
163
+
153
164
  ---
154
165
 
155
166
  ## 🤝 Contributing
package/assets/og.png ADDED
Binary file
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.js CHANGED
@@ -12,6 +12,20 @@ const RULE_WEIGHTS = {
12
12
  'magic-number': { severity: 'info', weight: 3 },
13
13
  'any-abuse': { severity: 'warning', weight: 8 },
14
14
  };
15
+ function hasIgnoreComment(file, line) {
16
+ const lines = file.getFullText().split('\n');
17
+ const currentLine = lines[line - 1] ?? '';
18
+ const prevLine = lines[line - 2] ?? '';
19
+ if (/\/\/\s*drift-ignore\b/.test(currentLine))
20
+ return true;
21
+ if (/\/\/\s*drift-ignore\b/.test(prevLine))
22
+ return true;
23
+ return false;
24
+ }
25
+ function isFileIgnored(file) {
26
+ const firstLines = file.getFullText().split('\n').slice(0, 10).join('\n');
27
+ return /\/\/\s*drift-ignore-file\b/.test(firstLines);
28
+ }
15
29
  function getSnippet(node, file) {
16
30
  const startLine = node.getStartLineNumber();
17
31
  const lines = file.getFullText().split('\n');
@@ -50,12 +64,15 @@ function detectLargeFunctions(file) {
50
64
  ];
51
65
  for (const fn of fns) {
52
66
  const lines = getFunctionLikeLines(fn);
67
+ const startLine = fn.getStartLineNumber();
53
68
  if (lines > 50) {
69
+ if (hasIgnoreComment(file, startLine))
70
+ continue;
54
71
  issues.push({
55
72
  rule: 'large-function',
56
73
  severity: 'error',
57
74
  message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
58
- line: fn.getStartLineNumber(),
75
+ line: startLine,
59
76
  column: fn.getStartLinePos(),
60
77
  snippet: getSnippet(fn, file),
61
78
  });
@@ -67,27 +84,32 @@ function detectDebugLeftovers(file) {
67
84
  const issues = [];
68
85
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
69
86
  const expr = call.getExpression().getText();
87
+ const line = call.getStartLineNumber();
70
88
  if (/^console\.(log|warn|error|debug|info)\b/.test(expr)) {
89
+ if (hasIgnoreComment(file, line))
90
+ continue;
71
91
  issues.push({
72
92
  rule: 'debug-leftover',
73
93
  severity: 'warning',
74
94
  message: `console.${expr.split('.')[1]} left in production code.`,
75
- line: call.getStartLineNumber(),
95
+ line,
76
96
  column: call.getStartLinePos(),
77
97
  snippet: getSnippet(call, file),
78
98
  });
79
99
  }
80
100
  }
81
101
  const lines = file.getFullText().split('\n');
82
- lines.forEach((line, i) => {
83
- if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(line)) {
102
+ lines.forEach((lineContent, i) => {
103
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
104
+ if (hasIgnoreComment(file, i + 1))
105
+ return;
84
106
  issues.push({
85
107
  rule: 'debug-leftover',
86
108
  severity: 'warning',
87
- message: `Unresolved marker found: ${line.trim().slice(0, 60)}`,
109
+ message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
88
110
  line: i + 1,
89
111
  column: 1,
90
- snippet: line.trim().slice(0, 120),
112
+ snippet: lineContent.trim().slice(0, 120),
91
113
  });
92
114
  }
93
115
  });
@@ -197,6 +219,13 @@ function calculateScore(issues) {
197
219
  return Math.min(100, raw);
198
220
  }
199
221
  export function analyzeFile(file) {
222
+ if (isFileIgnored(file)) {
223
+ return {
224
+ path: file.getFilePath(),
225
+ issues: [],
226
+ score: 0,
227
+ };
228
+ }
200
229
  const issues = [
201
230
  ...detectLargeFile(file),
202
231
  ...detectLargeFunctions(file),
package/dist/cli.js CHANGED
@@ -18,8 +18,9 @@ program
18
18
  .option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
19
19
  .action((targetPath, options) => {
20
20
  const resolvedPath = resolve(targetPath ?? '.');
21
- console.error(`\nScanning ${resolvedPath}...`);
21
+ process.stderr.write(`\nScanning ${resolvedPath}...\n`);
22
22
  const files = analyzeProject(resolvedPath);
23
+ process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
23
24
  const report = buildReport(resolvedPath, files);
24
25
  if (options.json) {
25
26
  process.stdout.write(JSON.stringify(report, null, 2));
@@ -30,6 +31,7 @@ program
30
31
  const md = formatMarkdown(report);
31
32
  const outPath = resolve(options.output);
32
33
  writeFileSync(outPath, md, 'utf8');
34
+ // drift-ignore
33
35
  console.error(`Report saved to ${outPath}`);
34
36
  }
35
37
  const minScore = Number(options.minScore);
package/dist/printer.js CHANGED
@@ -1,7 +1,11 @@
1
+ // drift-ignore-file
1
2
  import kleur from 'kleur';
3
+ import { scoreToGrade, severityIcon, scoreBar } from './utils.js';
2
4
  export function printConsole(report) {
5
+ const sep = kleur.gray(' ' + '─'.repeat(50));
3
6
  console.log();
4
- console.log(kleur.bold().white(' drift') + kleur.gray(' vibe coding debt detector'));
7
+ console.log(kleur.bold().white(' drift') + kleur.gray(' vibe coding debt detector'));
8
+ console.log(sep);
5
9
  console.log();
6
10
  const grade = scoreToGrade(report.totalScore);
7
11
  const scoreColor = report.totalScore === 0
@@ -9,8 +13,19 @@ export function printConsole(report) {
9
13
  : report.totalScore < 45
10
14
  ? kleur.yellow
11
15
  : kleur.red;
12
- console.log(` Score ${scoreColor().bold(String(report.totalScore).padStart(3))}${kleur.gray('/100')} ${grade.badge}`);
13
- console.log(kleur.gray(` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info`));
16
+ const bar = scoreBar(report.totalScore);
17
+ console.log(` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`);
18
+ const cleanFiles = report.totalFiles - report.files.length;
19
+ console.log(kleur.gray(` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`));
20
+ console.log();
21
+ // Top issues in header
22
+ const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3);
23
+ if (topRules.length > 0) {
24
+ const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`);
25
+ console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`);
26
+ console.log();
27
+ }
28
+ console.log(sep);
14
29
  console.log();
15
30
  if (report.files.length === 0) {
16
31
  console.log(kleur.green(' No drift detected. Clean codebase.'));
@@ -35,37 +50,11 @@ export function printConsole(report) {
35
50
  ` ` +
36
51
  kleur.white(issue.message));
37
52
  if (issue.snippet) {
38
- console.log(kleur.gray(` ${issue.snippet.split('\n')[0].slice(0, 100)}`));
53
+ const snippetIndent = ' ' + ' '.repeat(icon.length + 1);
54
+ console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`));
39
55
  }
40
56
  }
41
57
  console.log();
42
58
  }
43
- // Top drifting rules summary
44
- const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3);
45
- if (sorted.length > 0) {
46
- console.log(kleur.gray(' Top rules:'));
47
- for (const [rule, count] of sorted) {
48
- console.log(kleur.gray(` · ${rule}: ${count}`));
49
- }
50
- console.log();
51
- }
52
- }
53
- function scoreToGrade(score) {
54
- if (score === 0)
55
- return { badge: kleur.green('CLEAN') };
56
- if (score < 20)
57
- return { badge: kleur.green('LOW') };
58
- if (score < 45)
59
- return { badge: kleur.yellow('MODERATE') };
60
- if (score < 70)
61
- return { badge: kleur.red('HIGH') };
62
- return { badge: kleur.bold().red('CRITICAL') };
63
- }
64
- function severityIcon(s) {
65
- if (s === 'error')
66
- return '✖';
67
- if (s === 'warning')
68
- return '▲';
69
- return '◦';
70
59
  }
71
60
  //# sourceMappingURL=printer.js.map
package/dist/reporter.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { scoreToGradeText, severityIcon } from './utils.js';
1
2
  export function buildReport(targetPath, files) {
2
3
  const allIssues = files.flatMap((f) => f.issues);
3
4
  const byRule = {};
@@ -13,6 +14,7 @@ export function buildReport(targetPath, files) {
13
14
  files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
14
15
  totalIssues: allIssues.length,
15
16
  totalScore,
17
+ totalFiles: files.length,
16
18
  summary: {
17
19
  errors: allIssues.filter((i) => i.severity === 'error').length,
18
20
  warnings: allIssues.filter((i) => i.severity === 'warning').length,
@@ -22,7 +24,7 @@ export function buildReport(targetPath, files) {
22
24
  };
23
25
  }
24
26
  export function formatMarkdown(report) {
25
- const grade = scoreToGrade(report.totalScore);
27
+ const grade = scoreToGradeText(report.totalScore);
26
28
  const lines = [];
27
29
  lines.push(`# drift report`);
28
30
  lines.push(``);
@@ -62,7 +64,7 @@ export function formatMarkdown(report) {
62
64
  for (const issue of file.issues) {
63
65
  const icon = severityIcon(issue.severity);
64
66
  lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`);
65
- lines.push(`\`\`\``);
67
+ lines.push(`\`\`\`typescript`);
66
68
  lines.push(issue.snippet);
67
69
  lines.push(`\`\`\``);
68
70
  lines.push(``);
@@ -71,22 +73,4 @@ export function formatMarkdown(report) {
71
73
  }
72
74
  return lines.join('\n');
73
75
  }
74
- function scoreToGrade(score) {
75
- if (score === 0)
76
- return { badge: '✦ CLEAN', label: 'clean' };
77
- if (score < 20)
78
- return { badge: '◎ LOW', label: 'low' };
79
- if (score < 45)
80
- return { badge: '◈ MODERATE', label: 'moderate' };
81
- if (score < 70)
82
- return { badge: '◉ HIGH', label: 'high' };
83
- return { badge: '⬡ CRITICAL', label: 'critical' };
84
- }
85
- function severityIcon(s) {
86
- if (s === 'error')
87
- return '✖';
88
- if (s === 'warning')
89
- return '▲';
90
- return '◦';
91
- }
92
76
  //# sourceMappingURL=reporter.js.map
package/dist/types.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface DriftReport {
17
17
  files: FileReport[];
18
18
  totalIssues: number;
19
19
  totalScore: number;
20
+ totalFiles: number;
20
21
  summary: {
21
22
  errors: number;
22
23
  warnings: number;
@@ -0,0 +1,10 @@
1
+ import type { DriftIssue } from './types.js';
2
+ export interface Grade {
3
+ badge: string;
4
+ label: string;
5
+ }
6
+ export declare function scoreToGrade(score: number): Grade;
7
+ export declare function scoreToGradeText(score: number): Grade;
8
+ export declare function severityIcon(s: DriftIssue['severity']): string;
9
+ export declare function scoreBar(score: number, width?: number): string;
10
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,36 @@
1
+ import kleur from 'kleur';
2
+ export function scoreToGrade(score) {
3
+ if (score === 0)
4
+ return { badge: kleur.green('CLEAN'), label: 'clean' };
5
+ if (score < 20)
6
+ return { badge: kleur.green('LOW'), label: 'low' };
7
+ if (score < 45)
8
+ return { badge: kleur.yellow('MODERATE'), label: 'moderate' };
9
+ if (score < 70)
10
+ return { badge: kleur.red('HIGH'), label: 'high' };
11
+ return { badge: kleur.bold().red('CRITICAL'), label: 'critical' };
12
+ }
13
+ export function scoreToGradeText(score) {
14
+ if (score === 0)
15
+ return { badge: '✦ CLEAN', label: 'clean' };
16
+ if (score < 20)
17
+ return { badge: '◎ LOW', label: 'low' };
18
+ if (score < 45)
19
+ return { badge: '◈ MODERATE', label: 'moderate' };
20
+ if (score < 70)
21
+ return { badge: '◉ HIGH', label: 'high' };
22
+ return { badge: '⬡ CRITICAL', label: 'critical' };
23
+ }
24
+ export function severityIcon(s) {
25
+ if (s === 'error')
26
+ return '✖';
27
+ if (s === 'warning')
28
+ return '▲';
29
+ return '◦';
30
+ }
31
+ export function scoreBar(score, width = 20) {
32
+ const filled = Math.round((score / 100) * width);
33
+ const empty = width - filled;
34
+ return '█'.repeat(filled) + '░'.repeat(empty);
35
+ }
36
+ //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardbar/drift",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Detect silent technical debt left by AI-generated code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,7 +13,14 @@
13
13
  "start": "node dist/cli.js",
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
- "keywords": ["vibe-coding", "technical-debt", "ai", "cli", "typescript", "static-analysis"],
16
+ "keywords": [
17
+ "vibe-coding",
18
+ "technical-debt",
19
+ "ai",
20
+ "cli",
21
+ "typescript",
22
+ "static-analysis"
23
+ ],
17
24
  "author": "eduardbar",
18
25
  "license": "MIT",
19
26
  "dependencies": {
package/src/analyzer.ts CHANGED
@@ -26,6 +26,21 @@ const RULE_WEIGHTS: Record<string, { severity: DriftIssue['severity']; weight: n
26
26
 
27
27
  type FunctionLike = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration
28
28
 
29
+ function hasIgnoreComment(file: SourceFile, line: number): boolean {
30
+ const lines = file.getFullText().split('\n')
31
+ const currentLine = lines[line - 1] ?? ''
32
+ const prevLine = lines[line - 2] ?? ''
33
+
34
+ if (/\/\/\s*drift-ignore\b/.test(currentLine)) return true
35
+ if (/\/\/\s*drift-ignore\b/.test(prevLine)) return true
36
+ return false
37
+ }
38
+
39
+ function isFileIgnored(file: SourceFile): boolean {
40
+ const firstLines = file.getFullText().split('\n').slice(0, 10).join('\n')
41
+ return /\/\/\s*drift-ignore-file\b/.test(firstLines)
42
+ }
43
+
29
44
  function getSnippet(node: Node, file: SourceFile): string {
30
45
  const startLine = node.getStartLineNumber()
31
46
  const lines = file.getFullText().split('\n')
@@ -68,12 +83,14 @@ function detectLargeFunctions(file: SourceFile): DriftIssue[] {
68
83
 
69
84
  for (const fn of fns) {
70
85
  const lines = getFunctionLikeLines(fn)
86
+ const startLine = fn.getStartLineNumber()
71
87
  if (lines > 50) {
88
+ if (hasIgnoreComment(file, startLine)) continue
72
89
  issues.push({
73
90
  rule: 'large-function',
74
91
  severity: 'error',
75
92
  message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
76
- line: fn.getStartLineNumber(),
93
+ line: startLine,
77
94
  column: fn.getStartLinePos(),
78
95
  snippet: getSnippet(fn, file),
79
96
  })
@@ -87,12 +104,14 @@ function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
87
104
 
88
105
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
89
106
  const expr = call.getExpression().getText()
107
+ const line = call.getStartLineNumber()
90
108
  if (/^console\.(log|warn|error|debug|info)\b/.test(expr)) {
109
+ if (hasIgnoreComment(file, line)) continue
91
110
  issues.push({
92
111
  rule: 'debug-leftover',
93
112
  severity: 'warning',
94
113
  message: `console.${expr.split('.')[1]} left in production code.`,
95
- line: call.getStartLineNumber(),
114
+ line,
96
115
  column: call.getStartLinePos(),
97
116
  snippet: getSnippet(call, file),
98
117
  })
@@ -100,15 +119,16 @@ function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
100
119
  }
101
120
 
102
121
  const lines = file.getFullText().split('\n')
103
- lines.forEach((line, i) => {
104
- if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(line)) {
122
+ lines.forEach((lineContent, i) => {
123
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
124
+ if (hasIgnoreComment(file, i + 1)) return
105
125
  issues.push({
106
126
  rule: 'debug-leftover',
107
127
  severity: 'warning',
108
- message: `Unresolved marker found: ${line.trim().slice(0, 60)}`,
128
+ message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
109
129
  line: i + 1,
110
130
  column: 1,
111
- snippet: line.trim().slice(0, 120),
131
+ snippet: lineContent.trim().slice(0, 120),
112
132
  })
113
133
  }
114
134
  })
@@ -228,6 +248,14 @@ function calculateScore(issues: DriftIssue[]): number {
228
248
  }
229
249
 
230
250
  export function analyzeFile(file: SourceFile): FileReport {
251
+ if (isFileIgnored(file)) {
252
+ return {
253
+ path: file.getFilePath(),
254
+ issues: [],
255
+ score: 0,
256
+ }
257
+ }
258
+
231
259
  const issues: DriftIssue[] = [
232
260
  ...detectLargeFile(file),
233
261
  ...detectLargeFunctions(file),
package/src/cli.ts CHANGED
@@ -22,9 +22,9 @@ program
22
22
  .action((targetPath: string | undefined, options: { output?: string; json?: boolean; minScore: string }) => {
23
23
  const resolvedPath = resolve(targetPath ?? '.')
24
24
 
25
- console.error(`\nScanning ${resolvedPath}...`)
26
-
25
+ process.stderr.write(`\nScanning ${resolvedPath}...\n`)
27
26
  const files = analyzeProject(resolvedPath)
27
+ process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
28
28
  const report = buildReport(resolvedPath, files)
29
29
 
30
30
  if (options.json) {
@@ -38,6 +38,7 @@ program
38
38
  const md = formatMarkdown(report)
39
39
  const outPath = resolve(options.output)
40
40
  writeFileSync(outPath, md, 'utf8')
41
+ // drift-ignore
41
42
  console.error(`Report saved to ${outPath}`)
42
43
  }
43
44
 
package/src/printer.ts CHANGED
@@ -1,9 +1,14 @@
1
+ // drift-ignore-file
1
2
  import kleur from 'kleur'
2
- import type { DriftReport, DriftIssue } from './types.js'
3
+ import type { DriftReport } from './types.js'
4
+ import { scoreToGrade, severityIcon, scoreBar } from './utils.js'
3
5
 
4
6
  export function printConsole(report: DriftReport): void {
7
+ const sep = kleur.gray(' ' + '─'.repeat(50))
8
+
5
9
  console.log()
6
- console.log(kleur.bold().white(' drift') + kleur.gray(' vibe coding debt detector'))
10
+ console.log(kleur.bold().white(' drift') + kleur.gray(' vibe coding debt detector'))
11
+ console.log(sep)
7
12
  console.log()
8
13
 
9
14
  const grade = scoreToGrade(report.totalScore)
@@ -13,16 +18,30 @@ export function printConsole(report: DriftReport): void {
13
18
  ? kleur.yellow
14
19
  : kleur.red
15
20
 
21
+ const bar = scoreBar(report.totalScore)
16
22
  console.log(
17
- ` Score ${scoreColor().bold(String(report.totalScore).padStart(3))}${kleur.gray('/100')} ${grade.badge}`
23
+ ` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
18
24
  )
25
+
26
+ const cleanFiles = report.totalFiles - report.files.length
19
27
  console.log(
20
28
  kleur.gray(
21
- ` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info`
29
+ ` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
22
30
  )
23
31
  )
24
32
  console.log()
25
33
 
34
+ // Top issues in header
35
+ const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
36
+ if (topRules.length > 0) {
37
+ const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`)
38
+ console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`)
39
+ console.log()
40
+ }
41
+
42
+ console.log(sep)
43
+ console.log()
44
+
26
45
  if (report.files.length === 0) {
27
46
  console.log(kleur.green(' No drift detected. Clean codebase.'))
28
47
  console.log()
@@ -54,33 +73,10 @@ export function printConsole(report: DriftReport): void {
54
73
  kleur.white(issue.message)
55
74
  )
56
75
  if (issue.snippet) {
57
- console.log(kleur.gray(` ${issue.snippet.split('\n')[0].slice(0, 100)}`))
76
+ const snippetIndent = ' ' + ' '.repeat(icon.length + 1)
77
+ console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`))
58
78
  }
59
79
  }
60
80
  console.log()
61
81
  }
62
-
63
- // Top drifting rules summary
64
- const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
65
- if (sorted.length > 0) {
66
- console.log(kleur.gray(' Top rules:'))
67
- for (const [rule, count] of sorted) {
68
- console.log(kleur.gray(` · ${rule}: ${count}`))
69
- }
70
- console.log()
71
- }
72
- }
73
-
74
- function scoreToGrade(score: number): { badge: string } {
75
- if (score === 0) return { badge: kleur.green('CLEAN') }
76
- if (score < 20) return { badge: kleur.green('LOW') }
77
- if (score < 45) return { badge: kleur.yellow('MODERATE') }
78
- if (score < 70) return { badge: kleur.red('HIGH') }
79
- return { badge: kleur.bold().red('CRITICAL') }
80
- }
81
-
82
- function severityIcon(s: DriftIssue['severity']): string {
83
- if (s === 'error') return '✖'
84
- if (s === 'warning') return '▲'
85
- return '◦'
86
82
  }
package/src/reporter.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { FileReport, DriftReport, DriftIssue } from './types.js'
1
+ import type { FileReport, DriftReport } from './types.js'
2
+ import { scoreToGradeText, severityIcon } from './utils.js'
2
3
 
3
4
  export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
4
5
  const allIssues = files.flatMap((f) => f.issues)
@@ -19,6 +20,7 @@ export function buildReport(targetPath: string, files: FileReport[]): DriftRepor
19
20
  files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
20
21
  totalIssues: allIssues.length,
21
22
  totalScore,
23
+ totalFiles: files.length,
22
24
  summary: {
23
25
  errors: allIssues.filter((i) => i.severity === 'error').length,
24
26
  warnings: allIssues.filter((i) => i.severity === 'warning').length,
@@ -29,7 +31,7 @@ export function buildReport(targetPath: string, files: FileReport[]): DriftRepor
29
31
  }
30
32
 
31
33
  export function formatMarkdown(report: DriftReport): string {
32
- const grade = scoreToGrade(report.totalScore)
34
+ const grade = scoreToGradeText(report.totalScore)
33
35
  const lines: string[] = []
34
36
 
35
37
  lines.push(`# drift report`)
@@ -71,7 +73,7 @@ export function formatMarkdown(report: DriftReport): string {
71
73
  for (const issue of file.issues) {
72
74
  const icon = severityIcon(issue.severity)
73
75
  lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`)
74
- lines.push(`\`\`\``)
76
+ lines.push(`\`\`\`typescript`)
75
77
  lines.push(issue.snippet)
76
78
  lines.push(`\`\`\``)
77
79
  lines.push(``)
@@ -81,17 +83,3 @@ export function formatMarkdown(report: DriftReport): string {
81
83
 
82
84
  return lines.join('\n')
83
85
  }
84
-
85
- function scoreToGrade(score: number): { badge: string; label: string } {
86
- if (score === 0) return { badge: '✦ CLEAN', label: 'clean' }
87
- if (score < 20) return { badge: '◎ LOW', label: 'low' }
88
- if (score < 45) return { badge: '◈ MODERATE', label: 'moderate' }
89
- if (score < 70) return { badge: '◉ HIGH', label: 'high' }
90
- return { badge: '⬡ CRITICAL', label: 'critical' }
91
- }
92
-
93
- function severityIcon(s: DriftIssue['severity']): string {
94
- if (s === 'error') return '✖'
95
- if (s === 'warning') return '▲'
96
- return '◦'
97
- }
package/src/types.ts CHANGED
@@ -19,6 +19,7 @@ export interface DriftReport {
19
19
  files: FileReport[]
20
20
  totalIssues: number
21
21
  totalScore: number
22
+ totalFiles: number
22
23
  summary: {
23
24
  errors: number
24
25
  warnings: number
package/src/utils.ts ADDED
@@ -0,0 +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
+ }