@eduardbar/drift
v1.0.0
Published
Detect silent technical debt left by AI-generated code
Maintainers
Readme

drift
Detect technical debt in AI-generated TypeScript code. One command. Zero config.
Why · Installation · Commands · Rules · Score · Configuration · CI Integration · drift-ignore · Contributing
Why
AI coding tools ship code fast. They also leave behind consistent, predictable structural patterns that accumulate silently: files that grow to 600 lines, catch blocks that swallow errors, exports that nothing imports, functions duplicated across three modules because the model regenerated instead of reusing.
GitClear's 2024 analysis of 211M lines of code found a 39.9% drop in refactoring activity and an 8x increase in duplicated code blocks since AI tools became mainstream. A senior engineer on r/vibecoding put it plainly: "The code looks reviewed. It isn't. Nobody's reading 400-line files the AI dumped in one shot."
drift gives you a 0–100 score per file and project so you know what to look at before it reaches production.
How drift compares to existing tools:
| Tool | What it does | What it misses | |------|--------------|----------------| | ESLint | Correctness and style within a single file | Structural patterns, cross-file dead code, architecture violations | | SonarQube | Enterprise-grade static analysis | Costs money, requires infrastructure, overwhelming for small teams | | drift | Structural debt + AI-specific patterns + cross-file analysis + 0–100 score | Not a linter — does not replace ESLint |
Installation
# Run without installing
npx @eduardbar/drift scan .
# Install globally
npm install -g @eduardbar/drift
# Install as a dev dependency
npm install --save-dev @eduardbar/driftCommands
drift scan [path]
Scan a directory and print a scored report to stdout.
drift scan .
drift scan ./src
drift scan ./src --output report.md
drift scan ./src --json
drift scan ./src --ai
drift scan ./src --fix
drift scan ./src --min-score 50Options:
| Flag | Description |
|------|-------------|
| --output <file> | Write Markdown report to a file instead of stdout |
| --json | Output raw DriftReport JSON |
| --ai | Output structured JSON optimized for LLM consumption (Claude, GPT, etc.) |
| --fix | Print inline fix suggestions for each detected issue |
| --min-score <n> | Exit with code 1 if the overall score meets or exceeds this threshold |
Example output:
drift — technical debt detector
──────────────────────────────────────────────────
Score █████████████░░░░░░░ 67/100 HIGH
4 file(s) with issues · 5 errors · 12 warnings · 3 info · 18 files clean
Top issues: debug-leftover ×8 · any-abuse ×5 · no-return-type ×3
──────────────────────────────────────────────────
src/api/users.ts (score 85/100)
✖ L1 large-file File has 412 lines (threshold: 300)
▲ L34 debug-leftover console.log left in production code
▲ L89 catch-swallow Empty catch block silently swallows errors
▲ L201 any-abuse Explicit 'any' type detected
src/utils/helpers.ts (score 70/100)
✖ L12 duplicate-function-name 'formatDate' looks like a duplicate
▲ L55 dead-code Unused import 'debounce'drift diff [ref]
Compare the current project state against any git ref. Defaults to HEAD~1.
drift diff # HEAD vs HEAD~1
drift diff HEAD~3 # HEAD vs 3 commits ago
drift diff main # HEAD vs branch main
drift diff abc1234 # HEAD vs a specific commit
drift diff --json # Output raw JSON diffOptions:
| Flag | Description |
|------|-------------|
| --json | Output raw JSON diff |
Shows score delta, issues introduced, and issues resolved since the given ref.
drift report [path]
Generate a self-contained HTML report. No server required — open in any browser.
drift report # scan current directory
drift report ./src # scan specific path
drift report ./src --output my-report.htmlOptions:
| Flag | Description |
|------|-------------|
| --output <file> | Output path for the HTML file (default: drift-report.html) |
All styles and data are embedded inline in the output file.
drift badge [path]
Generate a badge.svg with the current score, compatible with shields.io style.
drift badge # writes badge.svg to current directory
drift badge ./src
drift badge ./src --output ./assets/drift-badge.svgOptions:
| Flag | Description |
|------|-------------|
| --output <file> | Output path for the SVG file (default: badge.svg) |
Add the badge to your README — see README Badge.
drift ci [path]
Emit GitHub Actions annotations and a step summary. Designed to run inside a CI workflow.
drift ci # scan current directory
drift ci ./src
drift ci ./src --min-score 60Options:
| Flag | Description |
|------|-------------|
| --min-score <n> | Exit with code 1 if the overall score meets or exceeds this threshold |
Outputs ::error and ::warning annotations visible in the PR diff. Writes a markdown summary to $GITHUB_STEP_SUMMARY.
drift trend [period]
Show score evolution over time. period accepts: week, month, quarter, year.
drift trend week
drift trend month
drift trend quarter --since 2025-01-01
drift trend year --until 2025-12-31Options:
| Flag | Description |
|------|-------------|
| --since <date> | Start date for the trend window (ISO 8601) |
| --until <date> | End date for the trend window (ISO 8601) |
drift blame [target]
Identify which files, rules, or contributors are responsible for the most debt. target accepts: file, rule, overall.
drift blame file # top files by score
drift blame rule # top rules by frequency
drift blame overall
drift blame file --top 10Options:
| Flag | Description |
|------|-------------|
| --top <n> | Limit output to top N results (default: 5) |
Rules
26 rules across three severity levels. All run automatically unless marked as requiring configuration.
| Rule | Severity | Weight | What it detects |
|------|----------|--------|-----------------|
| large-file | error | 20 | Files exceeding 300 lines — AI generates monolithic files instead of splitting responsibility |
| large-function | error | 15 | Functions exceeding 50 lines — AI avoids decomposing logic into smaller units |
| duplicate-function-name | error | 18 | Function names that appear more than once (case-insensitive) — AI regenerates helpers instead of reusing them |
| high-complexity | error | 15 | Cyclomatic complexity above 10 — AI produces correct code, not necessarily simple code |
| circular-dependency | error | 14 | Circular import chains between modules — AI doesn't reason about module topology |
| layer-violation | error | 16 | Imports that cross architectural layers in the wrong direction (e.g., domain importing from infra) — requires drift.config.ts |
| debug-leftover | warning | 10 | console.log, console.warn, console.error, and TODO / FIXME / HACK comments — AI leaves scaffolding in place |
| dead-code | warning | 8 | Named imports that are never used in the file — AI imports broadly |
| any-abuse | warning | 8 | Explicit any type annotations — AI defaults to any when type inference is unclear |
| catch-swallow | warning | 10 | Empty catch blocks — AI makes code not throw without handling the error |
| comment-contradiction | warning | 12 | Comments that restate what the surrounding code already expresses — AI over-documents the obvious |
| deep-nesting | warning | 12 | Control flow nested more than 3 levels deep — results in code that is difficult to follow |
| too-many-params | warning | 8 | Functions with more than 4 parameters — AI avoids grouping related arguments into objects |
| high-coupling | warning | 10 | Files importing from more than 10 distinct modules — AI imports broadly without encapsulation |
| promise-style-mix | warning | 7 | async/await and .then() / .catch() used together in the same file — AI combines styles inconsistently |
| unused-export | warning | 8 | Named exports that are never imported anywhere in the project — cross-file dead code ESLint cannot detect |
| dead-file | warning | 10 | Files never imported by any other file in the project — invisible dead code |
| unused-dependency | warning | 6 | Packages listed in package.json with no corresponding import in source files |
| cross-boundary-import | warning | 10 | Imports that cross module boundaries outside the allowed list — requires drift.config.ts |
| hardcoded-config | warning | 10 | Hardcoded URLs, IP addresses, secrets, or connection strings — AI skips environment variable abstraction |
| inconsistent-error-handling | warning | 8 | Mixed try/catch and .catch() patterns in the same file — AI combines approaches without a consistent strategy |
| unnecessary-abstraction | warning | 7 | Wrapper functions or helpers that add no logic over what they wrap — AI over-engineers simple calls |
| naming-inconsistency | warning | 6 | Mixed camelCase and snake_case in the same module — AI forgets project conventions mid-generation |
| semantic-duplication | warning | 12 | Functions with structurally identical logic despite different names — detected via AST fingerprinting, not text comparison |
| no-return-type | info | 5 | Functions missing an explicit return type annotation |
| magic-number | info | 3 | Numeric literals used directly in logic without a named constant |
Score
Calculation: For each file, drift sums the weights of all detected issues, capped at 100. The project score is the average across all scanned files.
| Score | Grade | Meaning | |-------|-------|---------| | 0 | CLEAN | No issues found | | 1–19 | LOW | Minor issues — safe to ship | | 20–44 | MODERATE | Worth a review before merging | | 45–69 | HIGH | Significant structural debt detected | | 70–100 | CRITICAL | Review before this goes anywhere near production |
Configuration
drift runs with zero configuration. Architectural rules (layer-violation, cross-boundary-import) require a drift.config.ts (or .js / .json) at your project root:
import type { DriftConfig } from '@eduardbar/drift'
export default {
layers: [
{ name: 'domain', patterns: ['src/domain/**'], canImportFrom: [] },
{ name: 'app', patterns: ['src/app/**'], canImportFrom: ['domain'] },
{ name: 'infra', patterns: ['src/infra/**'], canImportFrom: ['domain', 'app'] },
],
boundaries: [
{ name: 'auth', root: 'src/modules/auth', allowedExternalImports: ['src/shared'] },
{ name: 'billing', root: 'src/modules/billing', allowedExternalImports: ['src/shared'] },
],
exclude: [
'src/generated/**',
'**/*.spec.ts',
],
rules: {
'large-file': { threshold: 400 }, // override default 300
'magic-number': 'off', // disable a rule
},
} satisfies DriftConfigWithout a config file, layer-violation and cross-boundary-import are silently skipped. All other rules run with their defaults.
CI Integration
Basic gate with scan
name: Drift
on: [pull_request]
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Check debt score
run: npx @eduardbar/drift scan ./src --min-score 60Exit code is 1 if the score meets or exceeds --min-score. Exit code 0 otherwise.
Annotations and step summary with drift ci
name: Drift
on: [pull_request]
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run drift
run: npx @eduardbar/drift ci ./src --min-score 60drift ci emits ::error and ::warning annotations that appear inline in the PR diff and writes a formatted summary to $GITHUB_STEP_SUMMARY. Use this when you want visibility beyond a pass/fail exit code.
drift-ignore
Suppress a single issue
Add // drift-ignore at the end of the flagged line or on the line immediately above it:
console.log(debugPayload) // drift-ignore// drift-ignore
const result: any = parse(input)Suppress an entire file
Add // drift-ignore-file anywhere in the first 10 lines of the file:
// drift-ignore-file
// This file contains intentional console output — not debug leftovers.When drift-ignore-file is present, analyzeFile() returns an empty report with score 0 for that file. Use this for files like loggers or CLI printers where console.* calls are intentional.
README Badge
Generate a badge from your project score and add it to your README:
drift badge . --output ./assets/drift-badge.svgThen reference it in your README:
The badge uses shields.io-compatible styling and color-codes automatically by grade: green for LOW, yellow for MODERATE, orange for HIGH, red for CRITICAL.
Contributing
Open an issue before starting significant work. Check existing issues first — use the bug report or feature request templates.
To add a new detection rule:
- Create a branch:
git checkout -b feat/rule-name - Add
"rule-name": <weight>toRULE_WEIGHTSinsrc/analyzer.ts - Implement AST detection logic using ts-morph in
analyzeFile() - Add a
fix_suggestionentry insrc/printer.ts - Update the rules table in
README.mdandAGENTS.md - Open a PR using the template in
.github/PULL_REQUEST_TEMPLATE.md
See CODE_OF_CONDUCT.md before participating.
Stack
| Package | Role |
|---------|------|
| ts-morph | AST traversal and TypeScript analysis |
| commander | CLI commands and flags |
| kleur | Terminal colors (zero dependencies) |
Runtime: Node.js 18+ · TypeScript 5.x · ES Modules · Supports TypeScript (.ts, .tsx) and JavaScript (.js, .jsx) files
License
MIT © eduardbar
