@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.
@@ -0,0 +1,58 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ push:
7
+ tags:
8
+ - 'v*'
9
+ workflow_dispatch:
10
+ inputs:
11
+ tag:
12
+ description: 'Tag version to publish (e.g., 0.2.0)'
13
+ required: true
14
+ type: string
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ publish:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v4
25
+ with:
26
+ ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.tag) || '' }}
27
+
28
+ - name: Setup Node.js
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: '20'
32
+ registry-url: 'https://registry.npmjs.org'
33
+ cache: 'npm'
34
+
35
+ - name: Verify version matches tag
36
+ run: |
37
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
38
+ TAG_VERSION="${{ inputs.tag }}"
39
+ else
40
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
41
+ fi
42
+ PKG_VERSION=$(node -p "require('./package.json').version")
43
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
44
+ echo "Error: Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
45
+ exit 1
46
+ fi
47
+ echo "Version check passed: $PKG_VERSION"
48
+
49
+ - name: Install dependencies
50
+ run: npm ci
51
+
52
+ - name: Build
53
+ run: npm run build
54
+
55
+ - name: Publish to npm
56
+ run: npm publish --access public
57
+ env:
58
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -25,10 +25,15 @@ drift scans your TypeScript/JavaScript codebase for the specific patterns AI too
25
25
  ```bash
26
26
  $ npx @eduardbar/drift scan ./src
27
27
 
28
- drift vibe coding debt detector
28
+ drift vibe coding debt detector
29
+ ──────────────────────────────────────────────────
29
30
 
30
- Score 67/100 HIGH
31
- 4 file(s) with issues · 5 errors · 12 warnings · 3 info
31
+ Score █████████████░░░░░░░ 67/100 HIGH
32
+ 4 file(s) with issues · 5 errors · 12 warnings · 3 info · 18 files clean
33
+
34
+ Top issues: debug-leftover ×8 · any-abuse ×5 · no-return-type ×3
35
+
36
+ ──────────────────────────────────────────────────
32
37
 
33
38
  src/api/users.ts (score 85/100)
34
39
  ✖ L1 large-file File has 412 lines (threshold: 300)
@@ -39,11 +44,6 @@ $ npx @eduardbar/drift scan ./src
39
44
  src/utils/helpers.ts (score 70/100)
40
45
  ✖ L12 duplicate-function-name 'formatDate' looks like a duplicate
41
46
  ▲ L55 dead-code Unused import 'debounce'
42
-
43
- Top rules:
44
- · debug-leftover: 8
45
- · any-abuse: 5
46
- · no-return-type: 3
47
47
  ```
48
48
 
49
49
  ---
@@ -67,11 +67,16 @@ npm install --save-dev @eduardbar/drift
67
67
  ## 🚀 Usage
68
68
 
69
69
  ```bash
70
- drift scan # Scan current directory
71
- drift scan ./src # Scan a specific path
72
- drift scan ./src --output report.md # Write Markdown report to file
73
- drift scan ./src --json # Output raw JSON
74
- drift scan ./src --min-score 50 # Exit code 1 if score > 50
70
+ # Recommended no install needed
71
+ npx @eduardbar/drift scan .
72
+ npx @eduardbar/drift scan ./src
73
+ npx @eduardbar/drift scan ./src --output report.md
74
+ npx @eduardbar/drift scan ./src --json
75
+ npx @eduardbar/drift scan ./src --min-score 50
76
+
77
+ # Install globally if you want the short 'drift' command
78
+ npm install -g @eduardbar/drift
79
+ drift scan .
75
80
  ```
76
81
 
77
82
  ### Options
@@ -150,6 +155,12 @@ npm run build
150
155
  node dist/cli.js scan ./src
151
156
  ```
152
157
 
158
+ Or without cloning:
159
+
160
+ ```bash
161
+ npx @eduardbar/drift scan .
162
+ ```
163
+
153
164
  ---
154
165
 
155
166
  ## 🤝 Contributing
package/assets/og.svg CHANGED
@@ -1,105 +1,105 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
2
- <defs>
3
- <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
- <stop offset="0%" style="stop-color:#0a0a0f"/>
5
- <stop offset="100%" style="stop-color:#0f0f1a"/>
6
- </linearGradient>
7
- <linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
8
- <stop offset="0%" style="stop-color:#6366f1"/>
9
- <stop offset="100%" style="stop-color:#8b5cf6"/>
10
- </linearGradient>
11
- <linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
12
- <stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
13
- <stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
14
- </linearGradient>
15
- <filter id="blur-glow">
16
- <feGaussianBlur stdDeviation="40" result="blur"/>
17
- <feComposite in="SourceGraphic" in2="blur" operator="over"/>
18
- </filter>
19
- <filter id="soft-glow">
20
- <feGaussianBlur stdDeviation="3" result="blur"/>
21
- <feComposite in="SourceGraphic" in2="blur" operator="over"/>
22
- </filter>
23
- </defs>
24
-
25
- <!-- Background -->
26
- <rect width="1200" height="630" fill="url(#bg)"/>
27
-
28
- <!-- Ambient glow top-left -->
29
- <ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
30
-
31
- <!-- Ambient glow bottom-right -->
32
- <ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
33
-
34
- <!-- Grid lines subtle -->
35
- <line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
36
- <line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
37
- <line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
38
- <line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
39
-
40
- <!-- Left accent bar -->
41
- <rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
42
-
43
- <!-- Main title "drift" -->
44
- <text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
45
-
46
- <!-- Subtitle -->
47
- <text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
48
-
49
- <!-- Terminal block right side -->
50
- <rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
51
-
52
- <!-- Terminal header bar -->
53
- <rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
54
- <rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
55
-
56
- <!-- Terminal dots -->
57
- <circle cx="710" cy="185" r="6" fill="#ff5f57"/>
58
- <circle cx="730" cy="185" r="6" fill="#febc2e"/>
59
- <circle cx="750" cy="185" r="6" fill="#28c840"/>
60
-
61
- <!-- Terminal text -->
62
- <text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
63
- <text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
64
-
65
- <text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
66
-
67
- <text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
68
- <text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
69
- <text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
70
-
71
- <text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
72
-
73
- <text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
74
- <text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
75
- <text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
76
- <text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
77
-
78
- <text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
79
- <text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
80
- <text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
81
-
82
- <text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
83
- <text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
84
- <text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
85
-
86
- <text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
87
- <text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
88
- <text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
89
-
90
- <!-- Badges row -->
91
- <rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
92
- <text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
93
-
94
- <rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
95
- <text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
96
-
97
- <rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
98
- <text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
99
-
100
- <!-- Author -->
101
- <text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
102
-
103
- <!-- Bottom accent line -->
104
- <rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
105
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#0a0a0f"/>
5
+ <stop offset="100%" style="stop-color:#0f0f1a"/>
6
+ </linearGradient>
7
+ <linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
8
+ <stop offset="0%" style="stop-color:#6366f1"/>
9
+ <stop offset="100%" style="stop-color:#8b5cf6"/>
10
+ </linearGradient>
11
+ <linearGradient id="glow" x1="0%" y1="0%" x2="100%" y2="100%">
12
+ <stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.15"/>
13
+ <stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:0.05"/>
14
+ </linearGradient>
15
+ <filter id="blur-glow">
16
+ <feGaussianBlur stdDeviation="40" result="blur"/>
17
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
18
+ </filter>
19
+ <filter id="soft-glow">
20
+ <feGaussianBlur stdDeviation="3" result="blur"/>
21
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
22
+ </filter>
23
+ </defs>
24
+
25
+ <!-- Background -->
26
+ <rect width="1200" height="630" fill="url(#bg)"/>
27
+
28
+ <!-- Ambient glow top-left -->
29
+ <ellipse cx="200" cy="200" rx="300" ry="250" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.8"/>
30
+
31
+ <!-- Ambient glow bottom-right -->
32
+ <ellipse cx="1050" cy="480" rx="280" ry="220" fill="url(#glow)" filter="url(#blur-glow)" opacity="0.6"/>
33
+
34
+ <!-- Grid lines subtle -->
35
+ <line x1="0" y1="210" x2="1200" y2="210" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
36
+ <line x1="0" y1="420" x2="1200" y2="420" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
37
+ <line x1="300" y1="0" x2="300" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
38
+ <line x1="900" y1="0" x2="900" y2="630" stroke="#ffffff" stroke-opacity="0.03" stroke-width="1"/>
39
+
40
+ <!-- Left accent bar -->
41
+ <rect x="72" y="180" width="4" height="270" rx="2" fill="url(#accent)"/>
42
+
43
+ <!-- Main title "drift" -->
44
+ <text x="102" y="310" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', monospace" font-size="120" font-weight="700" fill="#ffffff" letter-spacing="-4">drift</text>
45
+
46
+ <!-- Subtitle -->
47
+ <text x="104" y="370" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="24" font-weight="400" fill="#94a3b8" letter-spacing="0.5">Detect silent technical debt left by AI-generated code.</text>
48
+
49
+ <!-- Terminal block right side -->
50
+ <rect x="680" y="165" width="448" height="300" rx="12" fill="#0d0d18" stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
51
+
52
+ <!-- Terminal header bar -->
53
+ <rect x="680" y="165" width="448" height="40" rx="12" fill="#1a1a2e"/>
54
+ <rect x="680" y="185" width="448" height="20" fill="#1a1a2e"/>
55
+
56
+ <!-- Terminal dots -->
57
+ <circle cx="710" cy="185" r="6" fill="#ff5f57"/>
58
+ <circle cx="730" cy="185" r="6" fill="#febc2e"/>
59
+ <circle cx="750" cy="185" r="6" fill="#28c840"/>
60
+
61
+ <!-- Terminal text -->
62
+ <text x="700" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#6366f1">$</text>
63
+ <text x="716" y="235" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#e2e8f0"> npx @eduardbar/drift scan ./src</text>
64
+
65
+ <text x="700" y="265" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> drift — vibe coding debt detector</text>
66
+
67
+ <text x="700" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b"> Score </text>
68
+ <text x="755" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" font-weight="700" fill="#ef4444"> 67</text>
69
+ <text x="779" y="295" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="13" fill="#64748b">/100 HIGH</text>
70
+
71
+ <text x="700" y="320" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> 4 file(s) · 5 errors · 12 warnings</text>
72
+
73
+ <text x="700" y="352" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b"> src/api/users.ts</text>
74
+ <text x="700" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444"> ✖ </text>
75
+ <text x="720" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#ef4444">large-file</text>
76
+ <text x="800" y="371" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> 412 lines detected</text>
77
+
78
+ <text x="700" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
79
+ <text x="720" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">catch-swallow</text>
80
+ <text x="820" y="390" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> empty catch block</text>
81
+
82
+ <text x="700" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308"> ▲ </text>
83
+ <text x="720" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#eab308">any-abuse</text>
84
+ <text x="793" y="409" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#94a3b8"> explicit any type</text>
85
+
86
+ <text x="700" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#22c55e"> ◦ </text>
87
+ <text x="720" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#64748b">no-return-type</text>
88
+ <text x="830" y="440" font-family="'Cascadia Code', 'Fira Code', 'Consolas', monospace" font-size="11" fill="#475569"> missing return type</text>
89
+
90
+ <!-- Badges row -->
91
+ <rect x="104" y="405" width="100" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
92
+ <text x="154" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">TypeScript</text>
93
+
94
+ <rect x="214" y="405" width="72" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
95
+ <text x="250" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">ts-morph</text>
96
+
97
+ <rect x="296" y="405" width="52" height="26" rx="5" fill="#1e1e3a" stroke="#6366f1" stroke-opacity="0.4" stroke-width="1"/>
98
+ <text x="322" y="423" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#818cf8" text-anchor="middle">MIT</text>
99
+
100
+ <!-- Author -->
101
+ <text x="104" y="520" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="16" fill="#334155">github.com/eduardbar/drift</text>
102
+
103
+ <!-- Bottom accent line -->
104
+ <rect x="0" y="622" width="1200" height="8" fill="url(#accent)" opacity="0.7"/>
105
+ </svg>
package/dist/analyzer.js CHANGED
@@ -12,6 +12,20 @@ const RULE_WEIGHTS = {
12
12
  'magic-number': { severity: 'info', weight: 3 },
13
13
  'any-abuse': { severity: 'warning', weight: 8 },
14
14
  };
15
+ function hasIgnoreComment(file, line) {
16
+ const lines = file.getFullText().split('\n');
17
+ const currentLine = lines[line - 1] ?? '';
18
+ const prevLine = lines[line - 2] ?? '';
19
+ if (/\/\/\s*drift-ignore\b/.test(currentLine))
20
+ return true;
21
+ if (/\/\/\s*drift-ignore\b/.test(prevLine))
22
+ return true;
23
+ return false;
24
+ }
25
+ function isFileIgnored(file) {
26
+ const firstLines = file.getFullText().split('\n').slice(0, 10).join('\n');
27
+ return /\/\/\s*drift-ignore-file\b/.test(firstLines);
28
+ }
15
29
  function getSnippet(node, file) {
16
30
  const startLine = node.getStartLineNumber();
17
31
  const lines = file.getFullText().split('\n');
@@ -50,12 +64,15 @@ function detectLargeFunctions(file) {
50
64
  ];
51
65
  for (const fn of fns) {
52
66
  const lines = getFunctionLikeLines(fn);
67
+ const startLine = fn.getStartLineNumber();
53
68
  if (lines > 50) {
69
+ if (hasIgnoreComment(file, startLine))
70
+ continue;
54
71
  issues.push({
55
72
  rule: 'large-function',
56
73
  severity: 'error',
57
74
  message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
58
- line: fn.getStartLineNumber(),
75
+ line: startLine,
59
76
  column: fn.getStartLinePos(),
60
77
  snippet: getSnippet(fn, file),
61
78
  });
@@ -67,27 +84,32 @@ function detectDebugLeftovers(file) {
67
84
  const issues = [];
68
85
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
69
86
  const expr = call.getExpression().getText();
87
+ const line = call.getStartLineNumber();
70
88
  if (/^console\.(log|warn|error|debug|info)\b/.test(expr)) {
89
+ if (hasIgnoreComment(file, line))
90
+ continue;
71
91
  issues.push({
72
92
  rule: 'debug-leftover',
73
93
  severity: 'warning',
74
94
  message: `console.${expr.split('.')[1]} left in production code.`,
75
- line: call.getStartLineNumber(),
95
+ line,
76
96
  column: call.getStartLinePos(),
77
97
  snippet: getSnippet(call, file),
78
98
  });
79
99
  }
80
100
  }
81
101
  const lines = file.getFullText().split('\n');
82
- lines.forEach((line, i) => {
83
- if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(line)) {
102
+ lines.forEach((lineContent, i) => {
103
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
104
+ if (hasIgnoreComment(file, i + 1))
105
+ return;
84
106
  issues.push({
85
107
  rule: 'debug-leftover',
86
108
  severity: 'warning',
87
- message: `Unresolved marker found: ${line.trim().slice(0, 60)}`,
109
+ message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
88
110
  line: i + 1,
89
111
  column: 1,
90
- snippet: line.trim().slice(0, 120),
112
+ snippet: lineContent.trim().slice(0, 120),
91
113
  });
92
114
  }
93
115
  });
@@ -197,6 +219,13 @@ function calculateScore(issues) {
197
219
  return Math.min(100, raw);
198
220
  }
199
221
  export function analyzeFile(file) {
222
+ if (isFileIgnored(file)) {
223
+ return {
224
+ path: file.getFilePath(),
225
+ issues: [],
226
+ score: 0,
227
+ };
228
+ }
200
229
  const issues = [
201
230
  ...detectLargeFile(file),
202
231
  ...detectLargeFunctions(file),
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ program
31
31
  const md = formatMarkdown(report);
32
32
  const outPath = resolve(options.output);
33
33
  writeFileSync(outPath, md, 'utf8');
34
+ // drift-ignore
34
35
  console.error(`Report saved to ${outPath}`);
35
36
  }
36
37
  const minScore = Number(options.minScore);
package/dist/printer.js CHANGED
@@ -1,3 +1,4 @@
1
+ // drift-ignore-file
1
2
  import kleur from 'kleur';
2
3
  import { scoreToGrade, severityIcon, scoreBar } from './utils.js';
3
4
  export function printConsole(report) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eduardbar/drift",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Detect silent technical debt left by AI-generated code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/analyzer.ts CHANGED
@@ -26,6 +26,21 @@ const RULE_WEIGHTS: Record<string, { severity: DriftIssue['severity']; weight: n
26
26
 
27
27
  type FunctionLike = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration
28
28
 
29
+ function hasIgnoreComment(file: SourceFile, line: number): boolean {
30
+ const lines = file.getFullText().split('\n')
31
+ const currentLine = lines[line - 1] ?? ''
32
+ const prevLine = lines[line - 2] ?? ''
33
+
34
+ if (/\/\/\s*drift-ignore\b/.test(currentLine)) return true
35
+ if (/\/\/\s*drift-ignore\b/.test(prevLine)) return true
36
+ return false
37
+ }
38
+
39
+ function isFileIgnored(file: SourceFile): boolean {
40
+ const firstLines = file.getFullText().split('\n').slice(0, 10).join('\n')
41
+ return /\/\/\s*drift-ignore-file\b/.test(firstLines)
42
+ }
43
+
29
44
  function getSnippet(node: Node, file: SourceFile): string {
30
45
  const startLine = node.getStartLineNumber()
31
46
  const lines = file.getFullText().split('\n')
@@ -68,12 +83,14 @@ function detectLargeFunctions(file: SourceFile): DriftIssue[] {
68
83
 
69
84
  for (const fn of fns) {
70
85
  const lines = getFunctionLikeLines(fn)
86
+ const startLine = fn.getStartLineNumber()
71
87
  if (lines > 50) {
88
+ if (hasIgnoreComment(file, startLine)) continue
72
89
  issues.push({
73
90
  rule: 'large-function',
74
91
  severity: 'error',
75
92
  message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
76
- line: fn.getStartLineNumber(),
93
+ line: startLine,
77
94
  column: fn.getStartLinePos(),
78
95
  snippet: getSnippet(fn, file),
79
96
  })
@@ -87,12 +104,14 @@ function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
87
104
 
88
105
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
89
106
  const expr = call.getExpression().getText()
107
+ const line = call.getStartLineNumber()
90
108
  if (/^console\.(log|warn|error|debug|info)\b/.test(expr)) {
109
+ if (hasIgnoreComment(file, line)) continue
91
110
  issues.push({
92
111
  rule: 'debug-leftover',
93
112
  severity: 'warning',
94
113
  message: `console.${expr.split('.')[1]} left in production code.`,
95
- line: call.getStartLineNumber(),
114
+ line,
96
115
  column: call.getStartLinePos(),
97
116
  snippet: getSnippet(call, file),
98
117
  })
@@ -100,15 +119,16 @@ function detectDebugLeftovers(file: SourceFile): DriftIssue[] {
100
119
  }
101
120
 
102
121
  const lines = file.getFullText().split('\n')
103
- lines.forEach((line, i) => {
104
- if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(line)) {
122
+ lines.forEach((lineContent, i) => {
123
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
124
+ if (hasIgnoreComment(file, i + 1)) return
105
125
  issues.push({
106
126
  rule: 'debug-leftover',
107
127
  severity: 'warning',
108
- message: `Unresolved marker found: ${line.trim().slice(0, 60)}`,
128
+ message: `Unresolved marker found: ${lineContent.trim().slice(0, 60)}`,
109
129
  line: i + 1,
110
130
  column: 1,
111
- snippet: line.trim().slice(0, 120),
131
+ snippet: lineContent.trim().slice(0, 120),
112
132
  })
113
133
  }
114
134
  })
@@ -228,6 +248,14 @@ function calculateScore(issues: DriftIssue[]): number {
228
248
  }
229
249
 
230
250
  export function analyzeFile(file: SourceFile): FileReport {
251
+ if (isFileIgnored(file)) {
252
+ return {
253
+ path: file.getFilePath(),
254
+ issues: [],
255
+ score: 0,
256
+ }
257
+ }
258
+
231
259
  const issues: DriftIssue[] = [
232
260
  ...detectLargeFile(file),
233
261
  ...detectLargeFunctions(file),
package/src/cli.ts CHANGED
@@ -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
- console.error(`Report saved to ${outPath}`)
42
- }
43
-
44
- const minScore = Number(options.minScore)
45
- if (minScore > 0 && report.totalScore > minScore) {
46
- process.exit(1)
47
- }
48
- })
49
-
50
- program.parse()
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
- import kleur from 'kleur'
2
- import type { DriftReport } from './types.js'
3
- import { scoreToGrade, severityIcon, scoreBar } from './utils.js'
4
-
5
- export function printConsole(report: DriftReport): void {
6
- const sep = kleur.gray(' ' + '─'.repeat(50))
7
-
8
- console.log()
9
- console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'))
10
- console.log(sep)
11
- console.log()
12
-
13
- const grade = scoreToGrade(report.totalScore)
14
- const scoreColor = report.totalScore === 0
15
- ? kleur.green
16
- : report.totalScore < 45
17
- ? kleur.yellow
18
- : kleur.red
19
-
20
- const bar = scoreBar(report.totalScore)
21
- console.log(
22
- ` Score ${kleur.gray(bar)} ${scoreColor().bold(String(report.totalScore))}/100 ${grade.badge}`
23
- )
24
-
25
- const cleanFiles = report.totalFiles - report.files.length
26
- console.log(
27
- kleur.gray(
28
- ` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info · ${cleanFiles} files clean`
29
- )
30
- )
31
- console.log()
32
-
33
- // Top issues in header
34
- const topRules = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3)
35
- if (topRules.length > 0) {
36
- const parts = topRules.map(([rule, count]) => `${kleur.cyan(rule)} ${kleur.gray(`×${count}`)}`)
37
- console.log(` Top issues: ${parts.join(kleur.gray(' · '))}`)
38
- console.log()
39
- }
40
-
41
- console.log(sep)
42
- console.log()
43
-
44
- if (report.files.length === 0) {
45
- console.log(kleur.green(' No drift detected. Clean codebase.'))
46
- console.log()
47
- return
48
- }
49
-
50
- for (const file of report.files) {
51
- const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '')
52
- console.log(
53
- kleur.bold().white(` ${rel}`) +
54
- kleur.gray(` (score ${file.score}/100)`)
55
- )
56
-
57
- for (const issue of file.issues) {
58
- const icon = severityIcon(issue.severity)
59
- const colorFn = (s: string) =>
60
- issue.severity === 'error'
61
- ? kleur.red(s)
62
- : issue.severity === 'warning'
63
- ? kleur.yellow(s)
64
- : kleur.cyan(s)
65
-
66
- console.log(
67
- ` ${colorFn(icon)} ` +
68
- kleur.gray(`L${issue.line}`) +
69
- ` ` +
70
- colorFn(issue.rule) +
71
- ` ` +
72
- kleur.white(issue.message)
73
- )
74
- if (issue.snippet) {
75
- const snippetIndent = ' ' + ' '.repeat(icon.length + 1)
76
- console.log(kleur.gray(`${snippetIndent}${issue.snippet.split('\n')[0].slice(0, 120)}`))
77
- }
78
- }
79
- console.log()
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
+ }