@eduardbar/drift 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ ![drift — vibe coding debt detector](./assets/og.svg)
2
+
3
+ # drift
4
+
5
+ Detect silent technical debt left by AI-generated code. One command. Zero config.
6
+
7
+ _Vibe coding ships fast. drift tells you what it left behind._
8
+
9
+ ![npm](https://img.shields.io/npm/v/@eduardbar/drift?color=6366f1&label=npm)
10
+ ![license](https://img.shields.io/badge/license-MIT-green.svg)
11
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6.svg)
12
+ ![ts-morph](https://img.shields.io/badge/powered%20by-ts--morph-6366f1.svg)
13
+ ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
14
+
15
+ [Installation](#-installation) • [Usage](#-usage) • [Rules](#-what-it-detects) • [CI Integration](#-ci-integration) • [Score](#-score) • [Contributing](#-contributing)
16
+
17
+ ---
18
+
19
+ ## 🎯 Why?
20
+
21
+ You reviewed the AI-generated code today. Huge files, unused functions, empty catch blocks, duplicate helpers, `console.log` everywhere. It ran fine in dev. It will bite you in prod.
22
+
23
+ drift scans your TypeScript/JavaScript codebase for the specific patterns AI tools leave behind and gives you a score so you know where to look first.
24
+
25
+ ```bash
26
+ $ npx @eduardbar/drift scan ./src
27
+
28
+ drift — vibe coding debt detector
29
+
30
+ Score 67/100 HIGH
31
+ 4 file(s) with issues · 5 errors · 12 warnings · 3 info
32
+
33
+ src/api/users.ts (score 85/100)
34
+ ✖ L1 large-file File has 412 lines (threshold: 300)
35
+ ▲ L34 debug-leftover console.log left in production code
36
+ ▲ L89 catch-swallow Empty catch block silently swallows errors
37
+ ▲ L201 any-abuse Explicit 'any' type detected
38
+
39
+ src/utils/helpers.ts (score 70/100)
40
+ ✖ L12 duplicate-function-name 'formatDate' looks like a duplicate
41
+ ▲ L55 dead-code Unused import 'debounce'
42
+
43
+ Top rules:
44
+ · debug-leftover: 8
45
+ · any-abuse: 5
46
+ · no-return-type: 3
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 📦 Installation
52
+
53
+ ```bash
54
+ # Run without installing
55
+ npx @eduardbar/drift scan ./src
56
+
57
+ # Install globally
58
+ npm install -g @eduardbar/drift
59
+ drift scan ./src
60
+
61
+ # Install as dev dependency
62
+ npm install --save-dev @eduardbar/drift
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🚀 Usage
68
+
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
75
+ ```
76
+
77
+ ### Options
78
+
79
+ | Flag | Description |
80
+ |------|-------------|
81
+ | `--output <file>` | Write Markdown report to a file |
82
+ | `--json` | Output raw JSON instead of console output |
83
+ | `--min-score <n>` | Exit with code 1 if overall score exceeds threshold |
84
+
85
+ ---
86
+
87
+ ## 🔍 What it detects
88
+
89
+ | Rule | Severity | What it catches |
90
+ |------|----------|-----------------|
91
+ | `large-file` | error | Files over 300 lines — AI dumps everything into one place |
92
+ | `large-function` | error | Functions over 50 lines — AI avoids splitting logic |
93
+ | `debug-leftover` | warning | `console.log`, `TODO`, `FIXME`, `HACK` comments |
94
+ | `dead-code` | warning | Unused imports — AI imports more than it uses |
95
+ | `duplicate-function-name` | error | Near-identical function names — AI regenerates instead of reusing |
96
+ | `any-abuse` | warning | Explicit `any` type — AI defaults to `any` when it can't infer |
97
+ | `catch-swallow` | warning | Empty catch blocks — AI makes code "not throw" |
98
+ | `no-return-type` | info | Missing explicit return types on functions |
99
+
100
+ ---
101
+
102
+ ## ⚙️ CI Integration
103
+
104
+ Drop this into your GitHub Actions workflow to block merges when drift exceeds your threshold:
105
+
106
+ ```yaml
107
+ - name: Check for vibe coding drift
108
+ run: npx @eduardbar/drift scan ./src --min-score 60
109
+ ```
110
+
111
+ Exit code `1` if score exceeds `--min-score`. Exit code `0` otherwise.
112
+
113
+ ---
114
+
115
+ ## 📊 Score
116
+
117
+ | Score | Grade | Meaning |
118
+ |-------|-------|---------|
119
+ | 0 | CLEAN | No issues found |
120
+ | 1–19 | LOW | Minor issues, safe to ship |
121
+ | 20–44 | MODERATE | Worth a review before merging |
122
+ | 45–69 | HIGH | Significant structural debt detected |
123
+ | 70–100 | CRITICAL | Review before this goes anywhere near production |
124
+
125
+ ---
126
+
127
+ ## 🗂️ Project structure
128
+
129
+ ```
130
+ src/
131
+ ├── types.ts — DriftIssue, FileReport, DriftReport interfaces
132
+ ├── analyzer.ts — AST analysis with ts-morph, 8 detection rules
133
+ ├── reporter.ts — buildReport() + Markdown formatter
134
+ ├── printer.ts — Console output with color (kleur)
135
+ ├── index.ts — Public API re-exports
136
+ └── cli.ts — CLI entry point (Commander.js)
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 🧪 Run on yourself
142
+
143
+ drift passes its own scan with a MODERATE score — the `console.log` calls in `printer.ts` are intentional CLI output, not debug leftovers. We eat our own dog food.
144
+
145
+ ```bash
146
+ git clone https://github.com/eduardbar/drift
147
+ cd drift
148
+ npm install
149
+ npm run build
150
+ node dist/cli.js scan ./src
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 🤝 Contributing
156
+
157
+ PRs are welcome. If you find a new AI code pattern worth detecting, open an issue with an example and we'll add a rule.
158
+
159
+ 1. Fork the repo
160
+ 2. Create a branch: `git checkout -b feat/rule-name`
161
+ 3. Add your rule in `src/analyzer.ts`
162
+ 4. Open a PR
163
+
164
+ ---
165
+
166
+ ## 🧱 Stack
167
+
168
+ TypeScript · ts-morph · commander · kleur
169
+
170
+ ---
171
+
172
+ ## 📄 License
173
+
174
+ MIT © [eduardbar](https://github.com/eduardbar)
175
+
176
+ ---
177
+
178
+ _Built with mate by a developer who got tired of reviewing the same AI-generated patterns every week._
package/assets/og.svg ADDED
@@ -0,0 +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>
@@ -0,0 +1,5 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import type { FileReport } from './types.js';
3
+ export declare function analyzeFile(file: SourceFile): FileReport;
4
+ export declare function analyzeProject(targetPath: string): FileReport[];
5
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1,236 @@
1
+ import { Project, SyntaxKind, } from 'ts-morph';
2
+ // Rules and their drift score weight
3
+ const RULE_WEIGHTS = {
4
+ 'large-file': { severity: 'error', weight: 20 },
5
+ 'large-function': { severity: 'error', weight: 15 },
6
+ 'debug-leftover': { severity: 'warning', weight: 10 },
7
+ 'dead-code': { severity: 'warning', weight: 8 },
8
+ 'duplicate-function-name': { severity: 'error', weight: 18 },
9
+ 'comment-contradiction': { severity: 'warning', weight: 12 },
10
+ 'no-return-type': { severity: 'info', weight: 5 },
11
+ 'catch-swallow': { severity: 'warning', weight: 10 },
12
+ 'magic-number': { severity: 'info', weight: 3 },
13
+ 'any-abuse': { severity: 'warning', weight: 8 },
14
+ };
15
+ function getSnippet(node, file) {
16
+ const startLine = node.getStartLineNumber();
17
+ const lines = file.getFullText().split('\n');
18
+ return lines
19
+ .slice(Math.max(0, startLine - 1), startLine + 1)
20
+ .join('\n')
21
+ .trim()
22
+ .slice(0, 120);
23
+ }
24
+ function getFunctionLikeLines(node) {
25
+ return node.getEndLineNumber() - node.getStartLineNumber();
26
+ }
27
+ function detectLargeFile(file) {
28
+ const lineCount = file.getEndLineNumber();
29
+ if (lineCount > 300) {
30
+ return [
31
+ {
32
+ rule: 'large-file',
33
+ severity: 'error',
34
+ message: `File has ${lineCount} lines (threshold: 300). Large files are the #1 sign of AI-generated structural drift.`,
35
+ line: 1,
36
+ column: 1,
37
+ snippet: `// ${lineCount} lines total`,
38
+ },
39
+ ];
40
+ }
41
+ return [];
42
+ }
43
+ function detectLargeFunctions(file) {
44
+ const issues = [];
45
+ const fns = [
46
+ ...file.getFunctions(),
47
+ ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
48
+ ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
49
+ ...file.getClasses().flatMap((c) => c.getMethods()),
50
+ ];
51
+ for (const fn of fns) {
52
+ const lines = getFunctionLikeLines(fn);
53
+ if (lines > 50) {
54
+ issues.push({
55
+ rule: 'large-function',
56
+ severity: 'error',
57
+ message: `Function spans ${lines} lines (threshold: 50). AI tends to dump logic into single functions.`,
58
+ line: fn.getStartLineNumber(),
59
+ column: fn.getStartLinePos(),
60
+ snippet: getSnippet(fn, file),
61
+ });
62
+ }
63
+ }
64
+ return issues;
65
+ }
66
+ function detectDebugLeftovers(file) {
67
+ const issues = [];
68
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
69
+ const expr = call.getExpression().getText();
70
+ if (/^console\.(log|warn|error|debug|info)\b/.test(expr)) {
71
+ issues.push({
72
+ rule: 'debug-leftover',
73
+ severity: 'warning',
74
+ message: `console.${expr.split('.')[1]} left in production code.`,
75
+ line: call.getStartLineNumber(),
76
+ column: call.getStartLinePos(),
77
+ snippet: getSnippet(call, file),
78
+ });
79
+ }
80
+ }
81
+ const lines = file.getFullText().split('\n');
82
+ lines.forEach((line, i) => {
83
+ if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(line)) {
84
+ issues.push({
85
+ rule: 'debug-leftover',
86
+ severity: 'warning',
87
+ message: `Unresolved marker found: ${line.trim().slice(0, 60)}`,
88
+ line: i + 1,
89
+ column: 1,
90
+ snippet: line.trim().slice(0, 120),
91
+ });
92
+ }
93
+ });
94
+ return issues;
95
+ }
96
+ function detectDeadCode(file) {
97
+ const issues = [];
98
+ for (const imp of file.getImportDeclarations()) {
99
+ for (const named of imp.getNamedImports()) {
100
+ const name = named.getName();
101
+ const refs = file.getDescendantsOfKind(SyntaxKind.Identifier).filter((id) => id.getText() === name && id !== named.getNameNode());
102
+ if (refs.length === 0) {
103
+ issues.push({
104
+ rule: 'dead-code',
105
+ severity: 'warning',
106
+ message: `Unused import '${name}'. AI often imports more than it uses.`,
107
+ line: imp.getStartLineNumber(),
108
+ column: imp.getStartLinePos(),
109
+ snippet: getSnippet(imp, file),
110
+ });
111
+ }
112
+ }
113
+ }
114
+ return issues;
115
+ }
116
+ function detectDuplicateFunctionNames(file) {
117
+ const issues = [];
118
+ const seen = new Map();
119
+ const fns = file.getFunctions();
120
+ for (const fn of fns) {
121
+ const name = fn.getName();
122
+ if (!name)
123
+ continue;
124
+ const normalized = name.toLowerCase().replace(/[_-]/g, '');
125
+ if (seen.has(normalized)) {
126
+ issues.push({
127
+ rule: 'duplicate-function-name',
128
+ severity: 'error',
129
+ message: `Function '${name}' looks like a duplicate of a previously defined function. AI often generates near-identical helpers.`,
130
+ line: fn.getStartLineNumber(),
131
+ column: fn.getStartLinePos(),
132
+ snippet: getSnippet(fn, file),
133
+ });
134
+ }
135
+ else {
136
+ seen.set(normalized, fn.getStartLineNumber());
137
+ }
138
+ }
139
+ return issues;
140
+ }
141
+ function detectAnyAbuse(file) {
142
+ const issues = [];
143
+ for (const node of file.getDescendantsOfKind(SyntaxKind.AnyKeyword)) {
144
+ issues.push({
145
+ rule: 'any-abuse',
146
+ severity: 'warning',
147
+ message: `Explicit 'any' type detected. AI defaults to 'any' when it can't infer types properly.`,
148
+ line: node.getStartLineNumber(),
149
+ column: node.getStartLinePos(),
150
+ snippet: getSnippet(node, file),
151
+ });
152
+ }
153
+ return issues;
154
+ }
155
+ function detectCatchSwallow(file) {
156
+ const issues = [];
157
+ for (const tryCatch of file.getDescendantsOfKind(SyntaxKind.TryStatement)) {
158
+ const catchClause = tryCatch.getCatchClause();
159
+ if (!catchClause)
160
+ continue;
161
+ const block = catchClause.getBlock();
162
+ const stmts = block.getStatements();
163
+ if (stmts.length === 0) {
164
+ issues.push({
165
+ rule: 'catch-swallow',
166
+ severity: 'warning',
167
+ message: `Empty catch block silently swallows errors. Classic AI pattern to make code "not throw".`,
168
+ line: catchClause.getStartLineNumber(),
169
+ column: catchClause.getStartLinePos(),
170
+ snippet: getSnippet(catchClause, file),
171
+ });
172
+ }
173
+ }
174
+ return issues;
175
+ }
176
+ function detectMissingReturnTypes(file) {
177
+ const issues = [];
178
+ for (const fn of file.getFunctions()) {
179
+ if (!fn.getReturnTypeNode()) {
180
+ issues.push({
181
+ rule: 'no-return-type',
182
+ severity: 'info',
183
+ message: `Function '${fn.getName() ?? 'anonymous'}' has no explicit return type.`,
184
+ line: fn.getStartLineNumber(),
185
+ column: fn.getStartLinePos(),
186
+ snippet: getSnippet(fn, file),
187
+ });
188
+ }
189
+ }
190
+ return issues;
191
+ }
192
+ function calculateScore(issues) {
193
+ let raw = 0;
194
+ for (const issue of issues) {
195
+ raw += RULE_WEIGHTS[issue.rule]?.weight ?? 5;
196
+ }
197
+ return Math.min(100, raw);
198
+ }
199
+ export function analyzeFile(file) {
200
+ const issues = [
201
+ ...detectLargeFile(file),
202
+ ...detectLargeFunctions(file),
203
+ ...detectDebugLeftovers(file),
204
+ ...detectDeadCode(file),
205
+ ...detectDuplicateFunctionNames(file),
206
+ ...detectAnyAbuse(file),
207
+ ...detectCatchSwallow(file),
208
+ ...detectMissingReturnTypes(file),
209
+ ];
210
+ return {
211
+ path: file.getFilePath(),
212
+ issues,
213
+ score: calculateScore(issues),
214
+ };
215
+ }
216
+ export function analyzeProject(targetPath) {
217
+ const project = new Project({
218
+ skipAddingFilesFromTsConfig: true,
219
+ compilerOptions: { allowJs: true },
220
+ });
221
+ project.addSourceFilesAtPaths([
222
+ `${targetPath}/**/*.ts`,
223
+ `${targetPath}/**/*.tsx`,
224
+ `${targetPath}/**/*.js`,
225
+ `${targetPath}/**/*.jsx`,
226
+ `!${targetPath}/**/node_modules/**`,
227
+ `!${targetPath}/**/dist/**`,
228
+ `!${targetPath}/**/.next/**`,
229
+ `!${targetPath}/**/build/**`,
230
+ `!${targetPath}/**/*.d.ts`,
231
+ `!${targetPath}/**/*.test.*`,
232
+ `!${targetPath}/**/*.spec.*`,
233
+ ]);
234
+ return project.getSourceFiles().map(analyzeFile);
235
+ }
236
+ //# sourceMappingURL=analyzer.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,41 @@
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
+ const program = new Command();
9
+ program
10
+ .name('drift')
11
+ .description('Detect silent technical debt left by AI-generated code')
12
+ .version('0.1.0');
13
+ program
14
+ .command('scan [path]', { isDefault: true })
15
+ .description('Scan a directory for vibe coding drift')
16
+ .option('-o, --output <file>', 'Write report to a Markdown file')
17
+ .option('--json', 'Output raw JSON report')
18
+ .option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
19
+ .action((targetPath, options) => {
20
+ const resolvedPath = resolve(targetPath ?? '.');
21
+ console.error(`\nScanning ${resolvedPath}...`);
22
+ const files = analyzeProject(resolvedPath);
23
+ const report = buildReport(resolvedPath, files);
24
+ if (options.json) {
25
+ process.stdout.write(JSON.stringify(report, null, 2));
26
+ return;
27
+ }
28
+ printConsole(report);
29
+ if (options.output) {
30
+ const md = formatMarkdown(report);
31
+ const outPath = resolve(options.output);
32
+ writeFileSync(outPath, md, 'utf8');
33
+ console.error(`Report saved to ${outPath}`);
34
+ }
35
+ const minScore = Number(options.minScore);
36
+ if (minScore > 0 && report.totalScore > minScore) {
37
+ process.exit(1);
38
+ }
39
+ });
40
+ program.parse();
41
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,4 @@
1
+ export { analyzeProject, analyzeFile } from './analyzer.js';
2
+ export { buildReport, formatMarkdown } from './reporter.js';
3
+ export type { DriftReport, FileReport, DriftIssue } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { analyzeProject, analyzeFile } from './analyzer.js';
2
+ export { buildReport, formatMarkdown } from './reporter.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { DriftReport } from './types.js';
2
+ export declare function printConsole(report: DriftReport): void;
3
+ //# sourceMappingURL=printer.d.ts.map
@@ -0,0 +1,71 @@
1
+ import kleur from 'kleur';
2
+ export function printConsole(report) {
3
+ console.log();
4
+ console.log(kleur.bold().white(' drift') + kleur.gray(' — vibe coding debt detector'));
5
+ console.log();
6
+ const grade = scoreToGrade(report.totalScore);
7
+ const scoreColor = report.totalScore === 0
8
+ ? kleur.green
9
+ : report.totalScore < 45
10
+ ? kleur.yellow
11
+ : kleur.red;
12
+ console.log(` Score ${scoreColor().bold(String(report.totalScore).padStart(3))}${kleur.gray('/100')} ${grade.badge}`);
13
+ console.log(kleur.gray(` ${report.files.length} file(s) with issues · ${report.summary.errors} errors · ${report.summary.warnings} warnings · ${report.summary.infos} info`));
14
+ console.log();
15
+ if (report.files.length === 0) {
16
+ console.log(kleur.green(' No drift detected. Clean codebase.'));
17
+ console.log();
18
+ return;
19
+ }
20
+ for (const file of report.files) {
21
+ const rel = file.path.replace(report.targetPath, '').replace(/^[\\/]/, '');
22
+ console.log(kleur.bold().white(` ${rel}`) +
23
+ kleur.gray(` (score ${file.score}/100)`));
24
+ for (const issue of file.issues) {
25
+ const icon = severityIcon(issue.severity);
26
+ const colorFn = (s) => issue.severity === 'error'
27
+ ? kleur.red(s)
28
+ : issue.severity === 'warning'
29
+ ? kleur.yellow(s)
30
+ : kleur.cyan(s);
31
+ console.log(` ${colorFn(icon)} ` +
32
+ kleur.gray(`L${issue.line}`) +
33
+ ` ` +
34
+ colorFn(issue.rule) +
35
+ ` ` +
36
+ kleur.white(issue.message));
37
+ if (issue.snippet) {
38
+ console.log(kleur.gray(` ${issue.snippet.split('\n')[0].slice(0, 100)}`));
39
+ }
40
+ }
41
+ console.log();
42
+ }
43
+ // Top drifting rules summary
44
+ const sorted = Object.entries(report.summary.byRule).sort((a, b) => b[1] - a[1]).slice(0, 3);
45
+ if (sorted.length > 0) {
46
+ console.log(kleur.gray(' Top rules:'));
47
+ for (const [rule, count] of sorted) {
48
+ console.log(kleur.gray(` · ${rule}: ${count}`));
49
+ }
50
+ console.log();
51
+ }
52
+ }
53
+ function scoreToGrade(score) {
54
+ if (score === 0)
55
+ return { badge: kleur.green('CLEAN') };
56
+ if (score < 20)
57
+ return { badge: kleur.green('LOW') };
58
+ if (score < 45)
59
+ return { badge: kleur.yellow('MODERATE') };
60
+ if (score < 70)
61
+ return { badge: kleur.red('HIGH') };
62
+ return { badge: kleur.bold().red('CRITICAL') };
63
+ }
64
+ function severityIcon(s) {
65
+ if (s === 'error')
66
+ return '✖';
67
+ if (s === 'warning')
68
+ return '▲';
69
+ return '◦';
70
+ }
71
+ //# sourceMappingURL=printer.js.map
@@ -0,0 +1,4 @@
1
+ import type { FileReport, DriftReport } from './types.js';
2
+ export declare function buildReport(targetPath: string, files: FileReport[]): DriftReport;
3
+ export declare function formatMarkdown(report: DriftReport): string;
4
+ //# sourceMappingURL=reporter.d.ts.map