@eduardbar/drift 0.2.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.svg +105 -105
- package/dist/analyzer.js +35 -6
- package/dist/cli.js +1 -0
- package/dist/printer.js +1 -0
- package/package.json +1 -1
- package/src/analyzer.ts +34 -6
- package/src/cli.ts +51 -50
- package/src/printer.ts +82 -81
- package/src/reporter.ts +85 -85
- package/src/types.ts +29 -29
- package/src/utils.ts +35 -35
|
@@ -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.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
package/dist/printer.js
CHANGED
package/package.json
CHANGED
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
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander'
|
|
3
|
-
import { writeFileSync } from 'node:fs'
|
|
4
|
-
import { resolve } from 'node:path'
|
|
5
|
-
import { analyzeProject } from './analyzer.js'
|
|
6
|
-
import { buildReport, formatMarkdown } from './reporter.js'
|
|
7
|
-
import { printConsole } from './printer.js'
|
|
8
|
-
|
|
9
|
-
const program = new Command()
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.name('drift')
|
|
13
|
-
.description('Detect silent technical debt left by AI-generated code')
|
|
14
|
-
.version('0.1.0')
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.command('scan [path]', { isDefault: true })
|
|
18
|
-
.description('Scan a directory for vibe coding drift')
|
|
19
|
-
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
20
|
-
.option('--json', 'Output raw JSON report')
|
|
21
|
-
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
22
|
-
.action((targetPath: string | undefined, options: { output?: string; json?: boolean; minScore: string }) => {
|
|
23
|
-
const resolvedPath = resolve(targetPath ?? '.')
|
|
24
|
-
|
|
25
|
-
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
26
|
-
const files = analyzeProject(resolvedPath)
|
|
27
|
-
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
28
|
-
const report = buildReport(resolvedPath, files)
|
|
29
|
-
|
|
30
|
-
if (options.json) {
|
|
31
|
-
process.stdout.write(JSON.stringify(report, null, 2))
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
printConsole(report)
|
|
36
|
-
|
|
37
|
-
if (options.output) {
|
|
38
|
-
const md = formatMarkdown(report)
|
|
39
|
-
const outPath = resolve(options.output)
|
|
40
|
-
writeFileSync(outPath, md, 'utf8')
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { writeFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { analyzeProject } from './analyzer.js'
|
|
6
|
+
import { buildReport, formatMarkdown } from './reporter.js'
|
|
7
|
+
import { printConsole } from './printer.js'
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('drift')
|
|
13
|
+
.description('Detect silent technical debt left by AI-generated code')
|
|
14
|
+
.version('0.1.0')
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('scan [path]', { isDefault: true })
|
|
18
|
+
.description('Scan a directory for vibe coding drift')
|
|
19
|
+
.option('-o, --output <file>', 'Write report to a Markdown file')
|
|
20
|
+
.option('--json', 'Output raw JSON report')
|
|
21
|
+
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
|
|
22
|
+
.action((targetPath: string | undefined, options: { output?: string; json?: boolean; minScore: string }) => {
|
|
23
|
+
const resolvedPath = resolve(targetPath ?? '.')
|
|
24
|
+
|
|
25
|
+
process.stderr.write(`\nScanning ${resolvedPath}...\n`)
|
|
26
|
+
const files = analyzeProject(resolvedPath)
|
|
27
|
+
process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`)
|
|
28
|
+
const report = buildReport(resolvedPath, files)
|
|
29
|
+
|
|
30
|
+
if (options.json) {
|
|
31
|
+
process.stdout.write(JSON.stringify(report, null, 2))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
printConsole(report)
|
|
36
|
+
|
|
37
|
+
if (options.output) {
|
|
38
|
+
const md = formatMarkdown(report)
|
|
39
|
+
const outPath = resolve(options.output)
|
|
40
|
+
writeFileSync(outPath, md, 'utf8')
|
|
41
|
+
// drift-ignore
|
|
42
|
+
console.error(`Report saved to ${outPath}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const minScore = Number(options.minScore)
|
|
46
|
+
if (minScore > 0 && report.totalScore > minScore) {
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
program.parse()
|
package/src/printer.ts
CHANGED
|
@@ -1,81 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
console.log(
|
|
10
|
-
console.log(
|
|
11
|
-
console.log()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.log()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
1
|
+
// drift-ignore-file
|
|
2
|
+
import kleur from 'kleur'
|
|
3
|
+
import type { DriftReport } from './types.js'
|
|
4
|
+
import { scoreToGrade, severityIcon, scoreBar } from './utils.js'
|
|
5
|
+
|
|
6
|
+
export function printConsole(report: DriftReport): void {
|
|
7
|
+
const sep = kleur.gray(' ' + '─'.repeat(50))
|
|
8
|
+
|
|
9
|
+
console.log()
|
|
10
|
+
console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'))
|
|
11
|
+
console.log(sep)
|
|
12
|
+
console.log()
|
|
13
|
+
|
|
14
|
+
const grade = scoreToGrade(report.totalScore)
|
|
15
|
+
const scoreColor = report.totalScore === 0
|
|
16
|
+
? kleur.green
|
|
17
|
+
: report.totalScore < 45
|
|
18
|
+
? kleur.yellow
|
|
19
|
+
: kleur.red
|
|
20
|
+
|
|
21
|
+
const bar = scoreBar(report.totalScore)
|
|
22
|
+
console.log(
|
|
23
|
+
` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const cleanFiles = report.totalFiles - report.files.length
|
|
27
|
+
console.log(
|
|
28
|
+
kleur.gray(
|
|
29
|
+
` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
console.log()
|
|
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
|
+
|
|
45
|
+
if (report.files.length === 0) {
|
|
46
|
+
console.log(kleur.green(' No drift detected. Clean codebase.'))
|
|
47
|
+
console.log()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const file of report.files) {
|
|
52
|
+
const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '')
|
|
53
|
+
console.log(
|
|
54
|
+
kleur.bold().white(` ${rel}`) +
|
|
55
|
+
kleur.gray(` (score ${file.score}/100)`)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
for (const issue of file.issues) {
|
|
59
|
+
const icon = severityIcon(issue.severity)
|
|
60
|
+
const colorFn = (s: string) =>
|
|
61
|
+
issue.severity === 'error'
|
|
62
|
+
? kleur.red(s)
|
|
63
|
+
: issue.severity === 'warning'
|
|
64
|
+
? kleur.yellow(s)
|
|
65
|
+
: kleur.cyan(s)
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
` ${colorFn(icon)} ` +
|
|
69
|
+
kleur.gray(`L${issue.line}`) +
|
|
70
|
+
` ` +
|
|
71
|
+
colorFn(issue.rule) +
|
|
72
|
+
` ` +
|
|
73
|
+
kleur.white(issue.message)
|
|
74
|
+
)
|
|
75
|
+
if (issue.snippet) {
|
|
76
|
+
const snippetIndent = ' ' + ' '.repeat(icon.length + 1)
|
|
77
|
+
console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log()
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/reporter.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import type { FileReport, DriftReport } from './types.js'
|
|
2
|
-
import { scoreToGradeText, severityIcon } from './utils.js'
|
|
3
|
-
|
|
4
|
-
export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
|
|
5
|
-
const allIssues = files.flatMap((f) => f.issues)
|
|
6
|
-
const byRule: Record<string, number> = {}
|
|
7
|
-
|
|
8
|
-
for (const issue of allIssues) {
|
|
9
|
-
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const totalScore =
|
|
13
|
-
files.length > 0
|
|
14
|
-
? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
|
|
15
|
-
: 0
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
scannedAt: new Date().toISOString(),
|
|
19
|
-
targetPath,
|
|
20
|
-
files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
|
|
21
|
-
totalIssues: allIssues.length,
|
|
22
|
-
totalScore,
|
|
23
|
-
totalFiles: files.length,
|
|
24
|
-
summary: {
|
|
25
|
-
errors: allIssues.filter((i) => i.severity === 'error').length,
|
|
26
|
-
warnings: allIssues.filter((i) => i.severity === 'warning').length,
|
|
27
|
-
infos: allIssues.filter((i) => i.severity === 'info').length,
|
|
28
|
-
byRule,
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function formatMarkdown(report: DriftReport): string {
|
|
34
|
-
const grade = scoreToGradeText(report.totalScore)
|
|
35
|
-
const lines: string[] = []
|
|
36
|
-
|
|
37
|
-
lines.push(`# drift report`)
|
|
38
|
-
lines.push(``)
|
|
39
|
-
lines.push(`> Generated: ${new Date(report.scannedAt).toLocaleString()}`)
|
|
40
|
-
lines.push(`> Path: \`${report.targetPath}\``)
|
|
41
|
-
lines.push(``)
|
|
42
|
-
lines.push(`## Overall drift score: ${report.totalScore}/100 ${grade.badge}`)
|
|
43
|
-
lines.push(``)
|
|
44
|
-
lines.push(`| | Count |`)
|
|
45
|
-
lines.push(`|---|---|`)
|
|
46
|
-
lines.push(`| Errors | ${report.summary.errors} |`)
|
|
47
|
-
lines.push(`| Warnings | ${report.summary.warnings} |`)
|
|
48
|
-
lines.push(`| Info | ${report.summary.infos} |`)
|
|
49
|
-
lines.push(`| Files with issues | ${report.files.length} |`)
|
|
50
|
-
lines.push(`| Total issues | ${report.totalIssues} |`)
|
|
51
|
-
lines.push(``)
|
|
52
|
-
|
|
53
|
-
if (Object.keys(report.summary.byRule).length > 0) {
|
|
54
|
-
lines.push(`## Issues by rule`)
|
|
55
|
-
lines.push(``)
|
|
56
|
-
const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1])
|
|
57
|
-
for (const [rule, count] of sorted) {
|
|
58
|
-
lines.push(`- \`${rule}\`: ${count}`)
|
|
59
|
-
}
|
|
60
|
-
lines.push(``)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (report.files.length === 0) {
|
|
64
|
-
lines.push(`## No drift detected`)
|
|
65
|
-
lines.push(``)
|
|
66
|
-
lines.push(`No issues found. Clean codebase.`)
|
|
67
|
-
} else {
|
|
68
|
-
lines.push(`## Files (sorted by drift score)`)
|
|
69
|
-
lines.push(``)
|
|
70
|
-
for (const file of report.files) {
|
|
71
|
-
lines.push(`### \`${file.path}\` — score ${file.score}/100`)
|
|
72
|
-
lines.push(``)
|
|
73
|
-
for (const issue of file.issues) {
|
|
74
|
-
const icon = severityIcon(issue.severity)
|
|
75
|
-
lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`)
|
|
76
|
-
lines.push(`\`\`\`typescript`)
|
|
77
|
-
lines.push(issue.snippet)
|
|
78
|
-
lines.push(`\`\`\``)
|
|
79
|
-
lines.push(``)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return lines.join('\n')
|
|
85
|
-
}
|
|
1
|
+
import type { FileReport, DriftReport } from './types.js'
|
|
2
|
+
import { scoreToGradeText, severityIcon } from './utils.js'
|
|
3
|
+
|
|
4
|
+
export function buildReport(targetPath: string, files: FileReport[]): DriftReport {
|
|
5
|
+
const allIssues = files.flatMap((f) => f.issues)
|
|
6
|
+
const byRule: Record<string, number> = {}
|
|
7
|
+
|
|
8
|
+
for (const issue of allIssues) {
|
|
9
|
+
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const totalScore =
|
|
13
|
+
files.length > 0
|
|
14
|
+
? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
|
|
15
|
+
: 0
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
scannedAt: new Date().toISOString(),
|
|
19
|
+
targetPath,
|
|
20
|
+
files: files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score),
|
|
21
|
+
totalIssues: allIssues.length,
|
|
22
|
+
totalScore,
|
|
23
|
+
totalFiles: files.length,
|
|
24
|
+
summary: {
|
|
25
|
+
errors: allIssues.filter((i) => i.severity === 'error').length,
|
|
26
|
+
warnings: allIssues.filter((i) => i.severity === 'warning').length,
|
|
27
|
+
infos: allIssues.filter((i) => i.severity === 'info').length,
|
|
28
|
+
byRule,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatMarkdown(report: DriftReport): string {
|
|
34
|
+
const grade = scoreToGradeText(report.totalScore)
|
|
35
|
+
const lines: string[] = []
|
|
36
|
+
|
|
37
|
+
lines.push(`# drift report`)
|
|
38
|
+
lines.push(``)
|
|
39
|
+
lines.push(`> Generated: ${new Date(report.scannedAt).toLocaleString()}`)
|
|
40
|
+
lines.push(`> Path: \`${report.targetPath}\``)
|
|
41
|
+
lines.push(``)
|
|
42
|
+
lines.push(`## Overall drift score: ${report.totalScore}/100 ${grade.badge}`)
|
|
43
|
+
lines.push(``)
|
|
44
|
+
lines.push(`| | Count |`)
|
|
45
|
+
lines.push(`|---|---|`)
|
|
46
|
+
lines.push(`| Errors | ${report.summary.errors} |`)
|
|
47
|
+
lines.push(`| Warnings | ${report.summary.warnings} |`)
|
|
48
|
+
lines.push(`| Info | ${report.summary.infos} |`)
|
|
49
|
+
lines.push(`| Files with issues | ${report.files.length} |`)
|
|
50
|
+
lines.push(`| Total issues | ${report.totalIssues} |`)
|
|
51
|
+
lines.push(``)
|
|
52
|
+
|
|
53
|
+
if (Object.keys(report.summary.byRule).length > 0) {
|
|
54
|
+
lines.push(`## Issues by rule`)
|
|
55
|
+
lines.push(``)
|
|
56
|
+
const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1])
|
|
57
|
+
for (const [rule, count] of sorted) {
|
|
58
|
+
lines.push(`- \`${rule}\`: ${count}`)
|
|
59
|
+
}
|
|
60
|
+
lines.push(``)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (report.files.length === 0) {
|
|
64
|
+
lines.push(`## No drift detected`)
|
|
65
|
+
lines.push(``)
|
|
66
|
+
lines.push(`No issues found. Clean codebase.`)
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(`## Files (sorted by drift score)`)
|
|
69
|
+
lines.push(``)
|
|
70
|
+
for (const file of report.files) {
|
|
71
|
+
lines.push(`### \`${file.path}\` — score ${file.score}/100`)
|
|
72
|
+
lines.push(``)
|
|
73
|
+
for (const issue of file.issues) {
|
|
74
|
+
const icon = severityIcon(issue.severity)
|
|
75
|
+
lines.push(`**${icon} [${issue.rule}]** Line ${issue.line}: ${issue.message}`)
|
|
76
|
+
lines.push(`\`\`\`typescript`)
|
|
77
|
+
lines.push(issue.snippet)
|
|
78
|
+
lines.push(`\`\`\``)
|
|
79
|
+
lines.push(``)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n')
|
|
85
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
export interface DriftIssue {
|
|
2
|
-
rule: string
|
|
3
|
-
severity: 'error' | 'warning' | 'info'
|
|
4
|
-
message: string
|
|
5
|
-
line: number
|
|
6
|
-
column: number
|
|
7
|
-
snippet: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface FileReport {
|
|
11
|
-
path: string
|
|
12
|
-
issues: DriftIssue[]
|
|
13
|
-
score: number // 0–100, higher = more drift
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface DriftReport {
|
|
17
|
-
scannedAt: string
|
|
18
|
-
targetPath: string
|
|
19
|
-
files: FileReport[]
|
|
20
|
-
totalIssues: number
|
|
21
|
-
totalScore: number
|
|
22
|
-
totalFiles: number
|
|
23
|
-
summary: {
|
|
24
|
-
errors: number
|
|
25
|
-
warnings: number
|
|
26
|
-
infos: number
|
|
27
|
-
byRule: Record<string, number>
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
export interface DriftIssue {
|
|
2
|
+
rule: string
|
|
3
|
+
severity: 'error' | 'warning' | 'info'
|
|
4
|
+
message: string
|
|
5
|
+
line: number
|
|
6
|
+
column: number
|
|
7
|
+
snippet: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FileReport {
|
|
11
|
+
path: string
|
|
12
|
+
issues: DriftIssue[]
|
|
13
|
+
score: number // 0–100, higher = more drift
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DriftReport {
|
|
17
|
+
scannedAt: string
|
|
18
|
+
targetPath: string
|
|
19
|
+
files: FileReport[]
|
|
20
|
+
totalIssues: number
|
|
21
|
+
totalScore: number
|
|
22
|
+
totalFiles: number
|
|
23
|
+
summary: {
|
|
24
|
+
errors: number
|
|
25
|
+
warnings: number
|
|
26
|
+
infos: number
|
|
27
|
+
byRule: Record<string, number>
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,35 +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
|
-
}
|
|
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
|
+
}
|