Why AI-generated apps fall apart in week three
It is never the first prompt that goes wrong. The decay has a mechanism, and once you can name it you can design around it.
The pattern is consistent enough to set your watch by. Week one is euphoric. Week two is productive. Somewhere in week three you make a small change and something unrelated stops working, and when you go looking for why, you find code you do not recognise doing a job you thought was handled somewhere else.
This is not bad luck and it is not a bad model. It is a mechanism, and it works the same way every time.
The mechanism
Every AI change to a codebase follows the same three steps: read the code, infer the intent, write the change. Step two is the whole problem.
Intent is not in the code. The code records what was decided, never why. When a model reads a repository it sees the what, reconstructs a plausible why, and edits on the basis of that reconstruction.
| What the code shows | What was actually meant | What the model may do |
|---|---|---|
| Two functions that validate an email | One is for public signup and deliberately stricter | Merge them, quietly loosening public signup |
| A retry loop around one API call | That upstream service is known to be flaky | Remove it as redundant complexity |
| A table with duplicated columns | Denormalised on purpose, for read performance | Normalise it, and slow the main query down |
| Frontend calls the API, never the database | A deliberate boundary: auditing lives in the API | Add a direct database call, because it is shorter |
Note what these have in common. None of them is a crash. Each is a reasonable change given only the code, and wrong given the reasoning nobody wrote down.
The four symptoms, in the order they appear
Duplication
The same job done in two places, because the model did not find the first one. Cheap on its own. Expensive because now a fix has to be applied twice and nobody knows that.
Boundary erosion
A layer that was supposed to be crossed one way starts being crossed several ways. Usually invisible until something that depended on the boundary (an audit log, a permission check, a cache) turns out to have a hole.
Orphaned code
Functions and files nothing calls any more, left behind by changes that routed around them. They make every future search noisier and every future change slower.
Fear
The terminal state. Nobody is confident what a change will do, so changes get bigger and rarer and riskier, and the project stops moving.
Why it accelerates
Decay is not linear, for a reason worth understanding: each generation reads the previous one's output as if it were intentional.
Change four introduces a duplicate validator. Change nine reads a codebase where validation happening in two places is now the observed convention, so it adds a third. Change fifteen sees a codebase with no consistent validation strategy and picks arbitrarily.
Yesterday's accident is today's precedent.
The model is not degrading. It is faithfully following a convention that was never a decision.
What does not fix it
- A better model. It reads the same absent information more thoroughly. The missing why is still missing.
- A longer prompt. It works for one change. The next session starts from the code again. See the thousand-line prompt problem.
- More tests. Genuinely valuable, and aimed at a different target. Tests pin behaviour; this is structure.
- A diagram in a document. Correct on the day it is drawn. Nothing connects it to the code, so it silently stops being true and is then worse than nothing, because people trust it.
What does
The intent has to exist somewhere outside the code, in a form that can be mechanically compared against the code. Those two properties together, not either alone.
- 01A structural description of the system: the parts, and which parts may talk to which.
- 02Rules that are checkable, not prose: "the frontend must not call payments directly" as something that fails a build.
- 03A comparison that runs on every change, so divergence is reported the day it happens rather than discovered in week twelve.
That is what Graphlit is. The drawing is the intent. It is typed, so it can be checked. The code is re-read after every change and compared back to it, and anything that no longer matches is reported.
Five nodes claim a file in this repo.
Press Check sync to hash every one of them against the code that is actually on disk right now.
Common questions
Why does AI-generated code get worse over time?
Because each change reads the previous output as if it were deliberate. An accidental pattern introduced early becomes the observed convention, and later changes reproduce it. The model is not degrading. It is faithfully copying something that was never a decision.
Can more tests prevent AI code from decaying?
They help, but they target the wrong layer. The common failure modes (duplicated logic, crossed architectural boundaries, orphaned code) all preserve behaviour, so behavioural tests pass. What is needed is a structural check about which parts may call which.
How do I know if my codebase is already drifting?
Three practical signals: you find two pieces of code doing the same job, you cannot confidently say which parts call which, or you avoid changing certain files. Any one of them means the map in your head has stopped matching the code.
Is this a problem with a specific AI tool?
No. It follows from generating code out of prompts against a codebase whose reasoning was never recorded, so it appears across every tool in the category. Tools differ in how quickly you reach it, not in whether you do.
Draw it. Then keep it true.
Graphlit turns an architecture drawing into a typed graph, builds against it, and proves the code still matches. Free to start.