I have two codebases in the same domain by the same author, one written by hand in 2025 and one written with heavy AI assistance in 2026, and the measurements say that AI-generated code is not too big. It is too distant: more files, more prefixes, more old shapes kept alive, and no pressure that ever forces something to be deleted.
The loop looks like this. Generated code needs generated tests to pin it down, because nobody read it closely enough to know what it does. The tests need generated docs to say why they exist. The docs drift, so you generate a gate to check them. The gate flags things, so you generate a sweep to clean up what it found. Each of those is real work product, and each is now something you maintain. Then you try to use the capability from a second place, the seam does not quite fit, and the cheapest fix is to generate an adapter.
The two codebases let me look at this with numbers instead of impressions. The first is a Telegram and GitHub bot I wrote by hand in 2025. The second is the one that ships today.
| Hand-written, 2025 | AI-assisted, 2026 | |
|---|---|---|
| Production Go | 17,267 lines | 134,226 lines |
| Packages | 39 | 123 |
| Test code | 7,735 lines (0.45:1) | 124,615 lines (0.93:1) |
| Surfaces | 2 | 7 |
| Model providers | 1 | 8 |
Is AI-generated code just bigger?
No, and I want to kill that read before it gets comfortable. The 2025 codebase was only 17,267 lines because it imported an engine that was already 113,931 lines. Counted honestly, the hand-written system was about 130,000 lines of Go the day it shipped. The successor is 134,226. Size did not move.
What moved is that two surfaces became seven and one model provider became eight. The bigger thing does more.
So the line counts are not the finding. The finding is what happened to my habits once typing got cheap.
Is AI-generated code harder to read?
Yes, and not because of line count. In the old codebase, creating a chat is 17 lines. Not a wrapper around the real thing, the whole operation: take the request, resolve who is asking, store the opening instruction, record the first message, return. Every line names something from the problem. Chat, instruction, message, identity. You read it once, top to bottom, and you are done. If it is wrong, it is wrong somewhere you can point at.
The equivalent entry point in the new codebase is four lines. Shorter. It dispatches into five concepts, and not one of them can be evaluated without leaving the file. The words are transport, driver, seam, envelope, dropped content kinds. All five are the implementation describing itself. None of them came from the problem.
Reading the 17 lines is reading a page. Reading the four is traversing a graph, and you come out holding a model of the machinery instead of a model of what the code does.
That tax shows up in no line count. There is not more code here. There is more distance per operation.
Does AI delete the old code when it refactors?
No. When I refactor by hand, the old shape goes away. That is most of the value. Afterwards there is one way the thing works, and the old way lives in the history, where it belongs.
AI-assisted refactoring does something else. It puts the new shape next to the old one, keeps both working, and writes a comment that reconciles them. There is a one-line function in the new codebase whose comment explains that it is byte-identical to the path that existed before a particular seam was introduced. The comment is accurate. It is also documenting a refactor that no longer exists anywhere except in that sentence. The function is a fossil with a plaque on it.
Do that for a few weeks and the codebase becomes sediment: every state it passed through, preserved in order, all of it compiling. The comments give it away first. They stop describing the thing and start justifying the diff. A comment that explains why this differs from what was here before is written for a reviewer who read the previous version, and that reviewer will never exist again.
Why do names get longer?
Nothing forces a package boundary, so none appears. One package in the new codebase holds 136 files, 15,236 lines and 1,002 functions in a single flat namespace. That one package is 88% of the entire production code of the hand-written system, which had 136 files in total, spread across 39 packages.
The cost turns up in the names, and I did not expect that. In that package, 54 function names begin with run, 20 with resolve, 18 with render and 15 with print. One small feature contributes twelve identifiers, eleven of which open by restating which feature they belong to.
The prefix is where the filing system should have been. It is a filing system typed out by hand, at every definition and every call site, forever. In the old codebase the equivalent type is ChatRequest in a package called chatservice. The package carries the qualifier so the name does not have to. That is not a style preference. It is the difference between paying for structure once and paying for it at every reference.
Why does nothing get removed?
Because nothing gets disproved. The old codebase was built in month-long slices, each with a written record of what it was for. Roughly 40% of that record is struck through and marked deprecated. Features built, shipped, used, then killed on purpose. The record shows a codebase getting smaller by intent, again and again.
There is no equivalent column in the new work and no equivalent act. Deletion did not get harder. What stopped firing is the thing that used to force the decision: I did not have time to maintain both. When adding is close to free, every capability that was ever plausible stays in the tree, still compiling, still in the suite, still in the docs, still costing.
Deletion was never a cleanup task. It was the feedback signal. Losing it is the expensive part.
What are the tests for?
Here is test code against source code in the new tree, by layer:
| Layer | Test lines per source line |
|---|---|
| Presentation surfaces | 1.11, 1.12, 0.89 |
| Capability layer | 0.57, 0.66 |
The parts a person looks at are tested about twice as heavily as the parts that do the work. That is backwards, and the reason is not carelessness. A surface is where behavior is observable without understanding the system, so it is where a test can be written confidently by someone, or something, that has not understood the system. The capability layer requires knowing what correct means, and there is no shortcut to that.
Some of those tests are the good kind. A test that remembers a specific failure, this input, this crash, never again, is worth more than the code it guards, and it should outlive several rewrites of that code. But most of the volume is not that. Most of it pins internal shape: this function is called with these arguments in this order. That is concrete poured around whatever shape happened to exist on the day it was poured.
The load-bearing test trap: code that cannot be read makes its tests load-bearing, because they become the only place the intended behavior is written down. Load-bearing tests make the shape permanent, because changing the shape now breaks the specification. So the unreadable thing becomes the thing you cannot change. Writing better tests does not break this loop. The tests are downstream of it.
What happens when you ask AI to restore working code?
It reinterprets. I asked for a shipped, working component to be brought back with one part of it removed. What came back had kept the replacement for the part that was supposed to go, and had dropped several pieces that were load-bearing. Each loss was documented in a header comment as a deliberate change.
The failure is not laziness. The work was careful and the comments were honest about what changed. The failure is that a plausible improvement is always available, and nothing in the process ever asks whether this was already solved.
Restoration and reinterpretation look identical from the inside. Someone who wrote the original recognises the shape and puts it back. Anything else rebuilds from the description, which is lossy, and what gets lost is exactly the parts that were there for a reason nobody wrote down.
What I do differently now
None of this is an argument against generation. I build an agent harness. I would not be shipping it if I thought the tools were a mistake, and generation is genuinely good at the part that used to be slow. What it does not do is the other half, and the other half is smaller than it sounds: deleting, bounding, naming.
In practice:
- Work in slices small enough to finish, and keep the written record. It matters more than it sounds like it should, because it is the only place a deprecation can be written down.
- Keep the deprecation column and use it. If nothing has been struck through this month, nothing has been disproved this month. That is information about the process, not about the code.
- When the shape changes, delete the old shape in the same change. Two shapes and a reconciling comment is not a refactor. It is a third thing to maintain.
- Make readability the acceptance criterion, not correctness. The tests will pass. The question to ask out loud is whether the next person can read the operation top to bottom without opening five files. If not, the change is not done, however green the run was.
- Keep the tests that remember a failure. Be suspicious of the ones that only remember a shape.
None of that is new. It is what everyone already knew about writing code by hand. Generation removed the friction that used to enforce it, so now it has to be enforced on purpose.
What changed in the year since: I stopped writing code
I stopped writing code by hand about a year ago. Not because the models got better. Because I could afford it. I started on Antigravity IDE v1, Google’s agent IDE, before Google pivoted it towards the 2.0 app.
Early in 2026, coding with AI was cheap: around $100 a month covered it for me. Now it is $200 to $400 a month in subscriptions, plus cloud credits I burn on top. That is my own bill, not anyone’s price list. What it buys is easy to undercount. The 134,226 lines of production Go above are what is left in one codebase after everything that got deleted, not what was written, and not the old streams of work that led there. Multiply that by the four to six projects running at once, and by the history an agent has to be able to read when it picks an old thread back up. The full Contenox system, every tree together, was about 0.4 million lines of code without docs. That is the trap in one number: when generating is cheap, you end up with more code than you can read, consume or test, and the gap between what exists and what anyone has verified is where the problems live.
Today I do not code. I oversee. Claude Code, DeepSeek Harness and Contenox itself write Contenox. I read what the envelope allowed, what the tests said, and the evidence a change carries.
That is where the lessons above stop being advice to a person with a keyboard and become rules for someone who only reviews. I cannot enforce readability by reading every line any more. The rules have to live in the harness: in what the envelope lets through, in the checks that run before a change lands, and in the evidence a change has to carry before I look at it.
Why I don’t want to be the maintainer
One maintainer who cannot keep up with model churn is how Aider stalled; its last release was in February 2026. My answer is to not be the maintainer. The agents write, and a human signs what lands.
The roadmap exists to make that arrangement safe: missions that run inside an envelope, evidence with every change (the intent, the diff, the tests run, the model used, the approvals given), and benchmarks replayed from my own merged pull requests, so a model or prompt change gets measured against code I already accepted.
Contenox
Contenox is a desktop app for building software with AI agents while staying accountable for the result. It is where the envelope, the evidence and the replayed benchmarks are being built; download it here.
Related guides
- How to review AI-written code: what to check, in order
- Git workflow with AI agents: branches, worktrees and undo
- What is an AI harness?: the part around the model that decides the result
- Aider: the stalled-maintainer case, in detail