@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.
- package/.github/workflows/publish.yml +58 -0
- package/README.md +24 -13
- package/assets/og.png +0 -0
- package/assets/og.svg +105 -105
- package/dist/analyzer.js +35 -6
- package/dist/cli.js +3 -1
- package/dist/printer.js +20 -31
- package/dist/reporter.js +4 -20
- package/dist/types.d.ts +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +36 -0
- package/package.json +9 -2
- package/src/analyzer.ts +34 -6
- package/src/cli.ts +3 -2
- package/src/printer.ts +25 -29
- package/src/reporter.ts +5 -17
- package/src/types.ts +1 -0
- package/src/utils.ts +35 -0
|
@@ -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
|
|
28
|
+
drift — vibe coding debt detector
|
|
29
|
+
──────────────────────────────────────────────────
|
|
29
30
|
|
|
30
|
-
Score
|
|
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
|
-
|
|
71
|
-
drift scan
|
|
72
|
-
drift scan ./src
|
|
73
|
-
drift scan ./src --
|
|
74
|
-
drift scan ./src --
|
|
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:
|
|
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
|
|
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((
|
|
83
|
-
if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(
|
|
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: ${
|
|
109
|
+
message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
|
|
88
110
|
line: i + 1,
|
|
89
111
|
column: 1,
|
|
90
|
-
snippet:
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
13
|
-
console.log(
|
|
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
|
-
|
|
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 =
|
|
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
package/dist/utils.d.ts
ADDED
|
@@ -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
|
|
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": [
|
|
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:
|
|
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
|
|
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((
|
|
104
|
-
if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(
|
|
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: ${
|
|
128
|
+
message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
|
|
109
129
|
line: i + 1,
|
|
110
130
|
column: 1,
|
|
111
|
-
snippet:
|
|
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
|
-
|
|
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
|
|
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('
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
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
|
+
}
|