The slopification of software engineering

Nobody decided to lower the bar. The bar just stopped being checked.
For twenty years, "shipping fast" meant cutting corners you could point to - skipped tests, a rushed migration, a hardcoded config you promised to fix next sprint. You knew what you'd traded away. Now the trade is invisible. You accept a suggestion, the build passes, the PR merges, and nobody - not you, not the reviewer, not the model - can fully explain why the code is shaped the way it is.
That's not velocity. That's slop, and it's accumulating in codebases faster than anyone's measuring it.
The tell isn't bad code. It's code nobody can explain.
Bad code has always existed. What's new is code that works and still nobody can account for.
Ask the author of a PR why a function retries three times instead of two, or why it checks a flag before a null check instead of after, and increasingly the honest answer is "the model wrote it that way and the tests passed." That's not a knowledge gap you can close with a code review comment. It's an entire PR built on vibes that happened to compile.
The old failure mode was a dev who didn't understand the system. The new one is a dev who understood it fine and still shipped something they can't defend, because defending it was never part of the loop. Generate, glance, accept. Repeat forty times before lunch.
You start to recognise the texture of it. A useEffect with three dependencies that should be two:
useEffect(() => {
fetchUserProfile(userId);
}, [userId, setProfile, navigate]); // why are setProfile and navigate here?A utility function that does the same thing as one already in the project, but with slightly different naming because the model didn't know it existed:
// src/utils/formatDate.ts - written by a human, six months ago
export const formatDate = (d: Date) => d.toLocaleDateString("en-US");
// src/helpers/dateFormatter.ts - generated last Tuesday, reviewed by nobody
export const dateFormatter = (date: Date) => date.toLocaleDateString("en-US");A try-catch that wraps everything and catches nothing usefully - doing just enough to look handled without actually handling anything:
try {
const res = await fetch("/api/payments", { method: "POST", body });
const data = await res.json();
return data;
} catch (error) {
console.log(error);
}If you've reviewed AI-assisted PRs, you've seen it. Code that doesn't look wrong. It just looks like nobody was home when it was written.
The aesthetic of slop
There's a visual register to AI-generated code that's becoming as recognisable as stock photography. You know it when you see it.

<div className="rounded-xl shadow-lg bg-gradient-to-r from-blue-500 to-purple-600
text-white font-semibold py-3 px-6 backdrop-blur-sm border border-white/10
hover:shadow-xl transition-all duration-300">
<h2 className="text-3xl font-bold bg-clip-text text-transparent
bg-gradient-to-r from-blue-200 to-purple-200">
Build something amazing today
</h2>
<p className="text-white/70 text-lg mt-2">
The all-in-one platform that helps you ship faster.
</p>
</div>Every button is a hero button. Every card has a drop shadow. Every colour is the same blue-to-purple gradient that the model apparently learned was "modern." If I have to review one more landing page where every section has rounded-2xl and a backdrop-blur for no reason, I'm closing my laptop and going outside.
It's not just visual. It's structural. Functions with four parameters when two would do, because the model generated the generic version instead of the one that fits your codebase. Components that re-implement a date picker instead of importing the one you already have in src/components/ui.
A 40-line switch statement that should be an object lookup, but the model has never seen your codebase's conventions, so it wrote the version it's seen most often in its training data: the verbose one.
// What the model generates
function getStatusLabel(status: string): string {
switch (status) {
case "pending":
return "Pending review";
case "approved":
return "Approved";
case "rejected":
return "Rejected";
case "in_review":
return "In review";
case "cancelled":
return "Cancelled";
case "expired":
return "Expired";
default:
return "Unknown";
}
}
// What already exists in your codebase
const STATUS_LABELS: Record<string, string> = {
pending: "Pending review",
approved: "Approved",
rejected: "Rejected",
in_review: "In review",
cancelled: "Cancelled",
expired: "Expired",
};The code works. It passes type checking. It even has comments - beautifully written comments that explain what the function does, in the confident tone of someone who's summarised ten thousand functions and has no idea why this particular one exists.
/**
* Processes the payment queue by iterating through pending transactions,
* validating each against the merchant configuration, and dispatching
* settlement requests to the appropriate payment gateway.
*
* @param queue - Array of pending payment transactions
* @returns Promise resolving to an array of settlement results
*/
async function processPaymentQueue(queue: Transaction[]): Promise<Result[]> {
return queue.map((t) => ({ id: t.id, status: "processed" })); // ???
}Review was built for human mistakes, not machine confidence
Code review assumes the person who wrote the code made a reasoning error - misunderstood a requirement, missed an edge case, forgot a lock. Reviewers are trained to spot that. They ask "did you think about X?" and the author either did or didn't.
LLM-generated code doesn't fail that way. It fails by being plausible. It has the shape of correct code - sensible variable names, matching types, a JSDoc comment that reads like documentation from a library you'd actually trust.
async function getUserOrders(userId: string): Promise<Order[]> {
const orders = await db.query(
`SELECT * FROM orders WHERE user_id = '${userId}' ORDER BY created_at DESC`
);
return orders.rows;
}Clean function name. Typed return. Descriptive parameter. Sorted results. Your reviewer sees the shape, nods, and moves on. The SQL injection sitting on line 3 sailed through because everything around it looked like code written by someone who knew what they were doing.
It's not that the reviewer missed a bug. It's that the code was optimised, by construction, to look like the kind of code that gets approved.
Your reviewer is a spell checker for reasoning. Great at catching a missing argument, useless against a function that's grammatically perfect and subtly wrong.
And the volume makes it worse. When one dev could write 200 lines a day, a careless reviewer caught up eventually - the backlog forced a slowdown that doubled as a safety net. When one dev can generate 2,000 lines before lunch and review is still budgeted at the old rate, something has to give. It's never the velocity.
"Just read the diff more carefully" doesn't scale
The obvious fix is discipline: read every line, question every suggestion, don't accept what you don't understand. That's correct advice and almost nobody follows it past week two, because the entire economic case for using the tool was speed, and vigilance is the thing speed trades against.
This is the same failure mode as "just write more tests" or "just be more careful with SQL." It's true. It's useless as a system. And it survives in retrospectives as advice everyone nods at and nobody operationalises. Telling a team to slow down and think harder about AI output is telling them to give back the exact gain they adopted the tool for. That instruction doesn't survive contact with a deadline.
The deeper problem is that "careful" isn't a skill you can sustain across thousands of accepts a week. Attention is a depletable resource. By accept #180, the eye that caught the bug in accept #12 has stopped looking as hard - not because the developer got lazier, but because vigilance decays and nothing in the workflow forces it to reset.
You know the feeling. Tab back to the diff. The model rewrote the component. It looks fine. The types match. You've been reviewing for forty minutes and this is the twelfth file. You're not reading anymore. You're scanning for shapes, and the shapes look right, and "LGTM" is two clicks away.
That's what happens when a human process meets machine-speed output.
Confidence without a mechanism doesn't hold up
There's a specific kind of trust forming around AI output that's worth naming directly: it feels like expertise because it arrives instantly and speaks fluently. But fluency was never what made expert code trustworthy. What made it trustworthy was that someone had internalised the failure modes and priced them in before hitting save.
A model hasn't priced anything in. It's pattern-matched against a distribution of code that includes the good, the mediocre, and the actively vulnerable, and it has no mechanism for knowing which bucket the current output landed in. Neither, most of the time, does the person accepting it - because the whole point of the interaction was not having to know.
That's the actual shift. Not "AI writes bad code" - it writes code across the same quality range humans do. The shift is that the checkpoint where a human used to notice quality has quietly moved from "before I write this" to "after it already shipped," if it exists at all.
What actually catches this: structure, not vigilance
The fix isn't a better prompt or a stricter reviewer policy. Vigilance-based fixes fail for the same reason they always have - they depend on the one resource that doesn't scale with volume.
What scales is something that runs on every accept, every save, every push, without needing anyone to remember to be careful that day.
That's the gap Iris Code is built to close. It sits in the loop where AI-generated code actually gets accepted - not as another chat window you have to remember to open, but as a structural check that runs whether or not anyone's paying close attention that afternoon. It flags the patterns that look plausible but carry known risk, surfaces the reasoning gaps a fluent-sounding diff tends to paper over, and does it at the speed code is actually being generated - not at the speed a human would need to slow down to catch it manually.
The goal isn't to slow AI-assisted development back down to pre-Copilot speed. It's to put a mechanism where the vigilance used to be, so speed doesn't have to be the thing you quietly trade quality for.
| Layer | Tool | What it does |
|---|---|---|
| Generation | Your AI assistant of choice | Produces code fast, with no built-in accountability for why it's shaped the way it is |
| Review | Human reviewer | Catches reasoning errors, struggles against plausible-looking output at volume |
| Structural check | Iris Code | Runs on every save, flags known risk patterns and quality gaps before they merge |
Slop isn't a quality problem you can review your way out of. It's a missing checkpoint. And the longer that checkpoint stays missing, the more your codebase starts to look like a feed - full of content that technically loads, technically runs, and means nothing to anyone who has to maintain it six months from now.
Now something's checking. So it doesn't have to be you, every single time, for every single accept.
Iris Code scores every file on save.
Get per-file health scores, security smell detection, and enforcement rules - directly in VS Code. Free to start, no account required.
Install the extension →