The AI Slop Index
VibeFix's definitive taxonomy of the 13 structural anti-patterns produced by AI coding assistants — each with a severity score, detection signals, and fix strategy.
TL;DR
The AI Slop Index is VibeFix's 0–100 composite score measuring AI code laziness across 13 distinct categories. Each category is scored 0 (clean), 1 (minor), or 2 (severe). The total produces a Slop Verdict: CLEAN (<20), FLAGGED (20–50), or CRITICAL (>50). The Slop Index runs automatically on every PR scan — no command needed.
Scoring Rubric
The 13 Slop Categories
Defensive Over-Engineering
Excessive guard clauses, null checks, and defensive logic for scenarios that cannot occur given the data contract. AI models add defensive code for every imaginable edge case regardless of context.
Detection Signals
- ›Null checks on values guaranteed by type system
- ›Try/catch around synchronous code
- ›Fallback values for required props
- ›Defensive array checks before .map()
Evidence Example
if (users && Array.isArray(users) && users !== null && users.length > 0) {
users.map(...)
}Fix Strategy
Trust your type system. Add defensive code only where the data contract genuinely allows null or failure — not where TypeScript already guarantees the shape.
Abstraction Theater
Unnecessary abstraction layers: wrapper functions with a single call inside, service classes with one method, context providers for state used in one place.
Detection Signals
- ›Wrappers that add no transformation
- ›Services that are thin proxies to a single library call
- ›Custom hooks that wrap a built-in hook with no logic
- ›Abstract base classes with one concrete implementation
Evidence Example
class UserService {
async getUser(id: string) {
return await db.user.findUnique({ where: { id } })
}
}Fix Strategy
Invert the abstraction rule: only abstract when you have two or more concrete use cases that diverge. A single use case requires no wrapper.
Copy-Paste Sprawl
Duplicated code blocks that differ by only a variable name or label. AI models regenerate rather than reference, producing identical logic in multiple locations.
Detection Signals
- ›Identical component structure with minor label variation
- ›Repeated API call patterns instead of a shared hook
- ›Duplicated validation logic across form handlers
- ›Copy-pasted CSS classes with single property change
Evidence Example
const handleEmailChange = (e) => setEmail(e.target.value) const handlePasswordChange = (e) => setPassword(e.target.value) const handleNameChange = (e) => setName(e.target.value)
Fix Strategy
Extract shared logic into a parameterized handler or utility. For forms, use a single handleChange factory: `const handleChange = (key) => (e) => setState(prev => ({ ...prev, [key]: e.target.value }))`
Dead Code Confidence
Exported functions, imported modules, and declared variables that are never used. AI generates complete implementations then discards them mid-session.
Detection Signals
- ›Unused imports not caught by linter
- ›Exported functions with no callers
- ›Feature flags that are always true
- ›Console.log statements left in production code
Evidence Example
import { useMemo, useCallback, useRef } from 'react' // only useState used
import { format, parseISO, addDays } from 'date-fns' // only format usedFix Strategy
Run eslint --rule no-unused-vars and remove everything flagged. Add this rule to CI to prevent recurrence.
Error Handling Theater
Try/catch blocks that swallow errors — catching exceptions but only logging to console, returning empty objects, or silently continuing. Looks like error handling, provides none.
Detection Signals
- ›catch(e) { console.log(e) }
- ›Returning {} or [] on error
- ›Toast notifications without re-throwing
- ›Empty catch blocks
Evidence Example
try {
const data = await fetchUser(id)
return data
} catch (e) {
console.error(e)
return {}
}Fix Strategy
Error handling must either recover (retry, fallback, user-facing message) or propagate (re-throw or return Result type). Silent swallowing is not error handling — it is error hiding.
Dependency Bloat
Installing full libraries for single utility functions available natively or in smaller packages. AI models default to the most popular package regardless of bundle impact.
Detection Signals
- ›lodash imported for _.get()
- ›moment.js for a single date format
- ›axios when fetch is sufficient
- ›uuid for crypto.randomUUID()
Evidence Example
import _ from 'lodash' const value = _.get(obj, 'user.name', 'Unknown') // Native: obj?.user?.name ?? 'Unknown'
Fix Strategy
Audit dependencies with bundlephobia.com. Replace single-function library imports with native equivalents or targeted imports (lodash/get vs lodash).
Fake Tests
Tests that assert but don't verify: checking that a function was called without verifying what it was called with, or testing that state changed without validating the value.
Detection Signals
- ›expect(fn).toHaveBeenCalled() without argument assertion
- ›Testing internal state rather than observable output
- ›Snapshots for dynamic data
- ›Tests that pass when the implementation is broken
Evidence Example
test('saves user', async () => {
await saveUser(mockUser)
expect(mockDb.save).toHaveBeenCalled() // no args checked
})Fix Strategy
Test observable outputs and side effects with exact arguments. Use `toHaveBeenCalledWith(exactArgs)`. Write the test to fail first (Red), then make it pass (Green).
Magic Constants
Hard-coded numeric and string literals with no semantic name. AI generates working values inline without creating named constants for business-meaningful numbers.
Detection Signals
- ›Timeout values like setTimeout(fn, 3000)
- ›Status codes like if (status === 403)
- ›Pixel values like marginTop: 72 for navbar height
- ›Threshold values like if (score > 75)
Evidence Example
setTimeout(() => setVisible(false), 3000)
if (retries > 3) throw new Error('Max retries')Fix Strategy
Extract all literals into named constants at module scope. `const TOAST_DURATION_MS = 3000` reads the intent; `3000` does not.
Hallucinated APIs
Calls to library methods, browser APIs, or framework features that do not exist. Less common but catastrophically high-impact — causes runtime crashes.
Detection Signals
- ›Library methods not in current version docs
- ›Browser APIs from proposal stage, not ratified
- ›Framework lifecycle hooks that were removed
- ›npm package exports that don't exist
Evidence Example
import { useServerAction } from 'next/server' // doesn't exist
navigator.clipboard.readText() // requires explicit permission grant, not available in all contextsFix Strategy
Verify every API call against the current version docs. For browser APIs, check MDN compatibility table. Never trust AI's documentation citations — they can be fabricated.
Type Washing
TypeScript used cosmetically: using `any` pervasively, casting with `as` to silence errors, or typing everything as `object`. Provides the appearance of type safety with none of the guarantees.
Detection Signals
- ›as unknown as TargetType patterns
- ›Pervasive any typing
- ›Response types typed as Record<string, any>
- ›@ts-ignore on more than 2 lines
Evidence Example
const data = response.data as any const user = data.user as User // no runtime validation
Fix Strategy
Use Zod or similar for runtime validation at trust boundaries. Replace `as` casts with type guards or schema parsing. Treat @ts-ignore as a code smell requiring comment justification.
Scaffold Rot
Boilerplate and scaffolding code left in place that the feature outgrew. Placeholder components, default config values, template README content, unused example files.
Detection Signals
- ›create-next-app default content in pages
- ›Unused public/ assets from initial scaffold
- ›Example environment variables still in .env
- ›Template comments like // Your component here
Evidence Example
// pages/index.tsx still has 'Get started by editing src/app/page.tsx' // README still says 'This is a Next.js project bootstrapped with create-next-app'
Fix Strategy
Audit the project for scaffold origin files. Remove or replace all template content before first production deployment.
Context Amnesia
Inconsistent naming, patterns, or architectural decisions across the codebase suggesting the AI lost context between sessions. The same concept is expressed three different ways in three different files.
Detection Signals
- ›Mixed naming conventions (camelCase and snake_case)
- ›Same utility function implemented in 3 files
- ›Inconsistent error handling patterns across routes
- ›Mixed async patterns (callbacks vs promises vs async/await)
Evidence Example
// auth.ts: const getUserData = async () => {}
// profile.ts: const get_user_data = () => new Promise()
// settings.ts: function getUserDataCallback(cb) {}Fix Strategy
Establish a project-wide conventions document before starting AI-assisted development. Run a consistency audit using VibeFix before merging across sessions.
Quick Reference
| # | Category | Prevalence | Impact |
|---|---|---|---|
| 01 | Comment Pollution | 89% | High |
| 02 | Defensive Over-Engineering | 81% | High |
| 03 | Abstraction Theater | 73% | High |
| 04 | Copy-Paste Sprawl | 67% | Medium |
| 05 | Dead Code Confidence | 49% | Medium |
| 06 | Error Handling Theater | 76% | Critical |
| 07 | Dependency Bloat | 35% | Medium |
| 08 | Fake Tests | 54% | Critical |
| 09 | Magic Constants | 61% | Medium |
| 10 | Hallucinated APIs | 12% | Critical |
| 11 | Type Washing | 43% | High |
| 12 | Scaffold Rot | 58% | Medium |
| 13 | Context Amnesia | 38% | Medium |
Run the Slop Index on your codebase
VibeFix runs all 13 categories automatically on every PR. Install PR Guardian or scan a live URL in 30 seconds.

Comment Pollution
Excessive inline comments that describe what the code does, not why. AI models narrate every line as a reflex, producing comments that add cognitive load without semantic value.
Detection Signals
Evidence Example
// Loop through the users array and for each user, check if active users.forEach(user => { if (user.active) { ... } })Fix Strategy
Delete any comment that can be replaced by reading the code. Reserve comments for non-obvious business logic, architectural trade-offs, and known gotchas.