Category: AI and Code Quality

  • Code Quality Is the Victim

    Code Quality Is the Victim

    AI still generates bad code. Bad system design, redundant logic, repeated components, unnecessary re-renders. That hasn’t changed. What changed, from what I’ve seen, is that people stopped looking.

    Let me walk through how we got here.

    The Autocomplete Era

    This was probably the best phase of AI-assisted development. You knew exactly what you wanted to write and where. The AI just typed it for you. If the suggestion was off, you saw it immediately and corrected it. You were still in the driver’s seat. The AI was a fast typist, nothing more.

    The code that came out of this era was mostly yours. You understood it because you were making every decision. The AI was filling in the blanks you already knew the answers to.

    Agents Enter the Picture

    Then Cursor and Claude Code showed up, and the dynamic shifted. Now the agent wasn’t just finishing your sentence. It was writing entire features.


    In the beginning, developers were still reviewing the output. The agent would do most of the work, but you’d scrutinize the result, find the issues, and fix them. And while you were in there fixing things, you’d notice other problems. A structural issue here. A questionable design decision there. You’d fix those too. The process of reviewing AI output was accidentally making developers engage deeply with the codebase. It wasn’t perfect, but the feedback loop still existed.

    Where We Are Now

    On some of the teams and projects I’ve been close to, the pattern looks like this: managers want fast results. Developers use agents to move fast. The feature works. The manager is happy. So the developer asks themselves: why would I review the code?

    They don’t. They move on to the next task.

    This isn’t because AI agents suddenly got better at writing clean, optimized code. They didn’t. What changed is two things: far fewer people are looking, and the code actually runs now. Early AI-generated code was broken in obvious ways. It had bugs you’d hit immediately, and that forced you back into the code to read it, understand it, and fix it. The broken output was accidentally a quality check. That’s no longer true. The code agents produce today mostly works. The feature does what it’s supposed to do. The tests pass. The manager clicks through the UI and sees no errors. So there’s no moment that forces the developer to look closely. When the output was broken, review was mandatory. Now that it runs, review feels optional. And under deadline pressure, optional things don’t happen.

    In codebases I’ve worked on, this is exactly what slips through: the page re-rendering three times when it should render once، the three separate loops over the same dataset that could’ve been one, the component copy-pasted into five different files because the agent didn’t know it already existed.

    The product works. Ship it.

    The problems are still there. They’re just hiding under working software.

    I don’t think this is entirely the developer’s fault. If you have ten tasks before a deadline and you stop to reason through every trade-off in every file the agent touched, you will not finish. The pressure is real. The incentive to just move is real.

    The Trade-off Nobody Names

    Here’s the honest version of what’s happening: many teams are trading codebase health for delivery speed, without acknowledging that it’s a trade-off at all.

    That’s the part that bothers me. The trade-off itself isn’t wrong. Sometimes you need to ship fast and clean up later. That’s a legitimate call. But when you don’t recognize it as a trade-off, you don’t plan for the cleanup. The technical debt doesn’t get scheduled. The codebase quietly rots. And then one day the cost of adding a new feature is three times what it should be, and nobody can explain why.

    You either keep the codebase clean and take longer to build, or you build fast and inherit a codebase you’ll eventually want to throw away and rewrite from scratch. Both paths are real. Both have costs.

    The mistake is pretending the second path has no cost just because the demo looks fine.

    The Rule I Work By Now

    I can’t fix the incentives of a whole industry, so I settled on a rule for my own work instead: I don’t ship agent-written code I can’t explain. Not a line-by-line audit of every file, but enough reading that I could tell a reviewer why each piece exists. If I can’t, I either read until I can, or I delete it and let the agent try again under my direction.

    And when a deadline genuinely forces me to skip that, I treat it as borrowing, not earning. The unreviewed code gets written down as debt, with a task and a date, instead of left as a silent hope. Making the trade-off is fine. Making it invisible is the part I refuse to do.

  • I Fixed a Bug I Never Found in AI-Generated Code

    I Fixed a Bug I Never Found in AI-Generated Code

    A few days ago, I was working on a fairly complicated feature: web sockets, AI live sessions, and user media interaction in the browser. I had been at it for about two weeks.

    At the start, I created a comprehensive plan with Claude Code. It looked solid. Implementing the plan took about an hour, fixing the initial bugs took another hour, and then I tested it. The feature was ostensibly working, so I moved on. The wrong decision I made: I never actually read the code.

    Over the following days, I kept layering more features on top of that foundation without reviewing a single line. Things looked like magic. Until they didn’t.

    When Everything Dropped at Once

    At a specific point in the implementation, everything stopped working. I tried solving it with AI. It got worse. I spent three days in that loop before accepting that no amount of prompting was going to find whatever was buried in there.

    Something was deeply wrong, and I had no idea where, because I had never actually understood the code I was building on.

    8 Hours of Reading My Own Codebase

    I stepped back and started walking through the flow from the beginning, following the request lifecycle and each socket event manually. No AI. Just me and the code.

    It took about 8 hours. Almost every line needed refactoring. I deleted a significant amount of logic: some of it I didn’t understand, some of it looked completely useless. I wasn’t sure which category some pieces fell into, but if I couldn’t reason about why something existed, I removed it.

    Then I ran the code.

    Everything worked perfectly.

    I never found the original bug. I just deleted it. It was hiding somewhere inside code I hadn’t written, hadn’t read, and didn’t understand, and the fix was making the codebase small enough that I could finally hold it in my head.

    What I Actually Learned

    AI got me moving fast. That part is real. But it also handed me a codebase I had no mental model of, and when something broke deep inside it, I had no way to reason about the problem. The tool that saved me an hour upfront cost me three days later.

    The failure wasn’t using AI. The failure was skipping the step where I actually understand what I’m building.

    Treating generated code as a draft to read and revise isn’t slower than trusting it blindly. It’s the opposite. Because the hour you spend reading is the insurance policy against the three days you’d spend debugging something you never understood in the first place.