Why AI keeps breaking features that already worked
The feature worked on Friday. On Monday a change to something unrelated broke it, and nothing in the toolchain said so. Here is why that keeps happening, and what actually helps.
AI coding tools break working features because every change is made from a fresh reading of the codebase, by something with no memory of why the code is the way it is and no map of what depends on what. The model edits the file in front of it; the failure appears two files away, in a feature nobody asked it to touch. The compiler is happy, the diff looks plausible, and the break surfaces days later when a user hits it.
That is the whole mechanism, stated up front. The rest of this article is the detail: why it is structural rather than a bug in any one tool, why the obvious fixes do not fix it, and what actually contains it.
The shape of the failure
Notice what kind of failure this is. It is not the first generation being wrong. First generations are where these tools shine. It is change number twelve breaking change number three: a regression, in the classic sense, except produced at a pace no human team could match.
The sequence is always some version of the same story. You ask for a change to the checkout. The agent, reading the code fresh, notices the checkout shares a helper with the signup flow. It "improves" the helper: renames a parameter, tightens a type, removes a branch that looked dead. Checkout works. Signup, which nobody tested because nobody touched it, is broken. Except somebody did touch it; they just could not see that they were touching it.
Three causes, all structural
1 · The model re-reads. It does not remember.
Every session starts from zero. The reasons behind the existing code live in conversations and heads, not in the files: why the branch that looks dead is load-bearing, why the duplication is deliberate. So the model reconstructs intent by inference, every time, and inference is sometimes wrong. This is the same mechanism behind why AI-generated apps rot; regressions are what it looks like change by change.
2 · Local edits have global consequences
Code is shared. One helper serves four features; one table feeds nine queries. An edit is made here, but its consequences land wherever the shared thing is used, and nothing shows the model, or you, that blast radius at the moment of the edit. The change is local. The damage is topological.
3 · Nothing bounds the change
Ask an agent to fix a button and it may reorganise imports in six files, because nothing tells it where the task ends. Instructions like "do not touch anything else" are advisory: a wish, not a boundary. The model weighs them against its training-shaped instinct to tidy, and the instinct often wins. Without a mechanical boundary, the scope of any change is whatever the model decides it is.
What does not fix it
- More tests. Tests pin the behaviour someone thought to pin. Regressions land precisely in the gap between what was tested and what was assumed, and a test suite says nothing about structure, so a helper quietly rewired underneath two features passes every test that does not exercise their intersection.
- Bigger context windows. Fitting the whole codebase into context means the model can read everything, not that it knows anything. Reading is not memory, and it is certainly not a record of intent. The guess gets better-informed; it stays a guess.
- Sterner prompts. "Be careful" and "change only this file" are inputs to a probability distribution. They shift it. They do not constrain it.
What actually contains it
Everything that works shares one property: it replaces an assumption with a check. These four are in rough order of effort, and the first two need no new tools at all.
One intent per change, and read the diff, not the result
Small changes make the blast radius reviewable. Before accepting anything, look at git diff --stat: the list of files touched is the first honest account of what actually happened. If you asked for a button fix and see six files, stop there. The interesting question is why, not whether the button works now.
Pin behaviour before changing anything near it
Before a change, add a test that captures what the neighbouring features currently do, the ones sharing files with your target. Characterisation tests are dull to write and they convert "nobody touched signup" from an assumption into something that fails loudly when it stops being true.
Make ownership explicit
Decide which files belong to which feature and write it down somewhere mechanical. Even a code-ownership file is a start. A change that strays outside its feature's files should be rejected by machinery, not caught by a tired reviewer. This is the single highest-leverage structural fix.
Compare structure after every change, not just behaviour
Tests ask "does it still behave?". The question that catches regressions early is "does it still have the shape we agreed?". Did an import cross a boundary, did a route stop existing, did a helper change hands. That requires a stated structure to compare against, which is what architecture drift detection is.
How Graphlit enforces the boundary
Graphlit makes step three and step four mechanical. Because the system is a typed graph and every node knows its files, a build task arrives with an explicit scope: the nodes it serves, and therefore an allowlist of files it may touch. A change that strays outside the allowlist is stopped. Not reviewed, stopped. After each task the checks run and the code is re-read and hash-compared back to the graph, so the task either lands green or is reverted.
Sketch → shapes
deterministic
Shapes → typed nodes
model
Infer what wasn't drawn
model
Graph → ordered work
deterministic
One task, then stop
model
Green or reverted
deterministic
Drawing still true?
deterministic
› 6 shapes, 5 arrows, 6 labels recovered from the board
The honest limit, stated on how it works and worth repeating here: a change can still be wrong inside its scope, in ways a typecheck cannot see. What the boundary buys is that the damage is contained to the feature you asked about, visible in the diff, and reversible as one unit, which is exactly the property the Monday-morning regression lacked.
If you want to see what an explicit scope looks like against your own project, import a repository. The graph, and the file ownership it implies, is the first thing you get.
Common questions
Why does AI keep breaking my existing code?
Because each change is made from a fresh reading of the codebase by a model with no memory of previous decisions and no map of what depends on what. It edits the file in front of it; the consequences land in features that share code with that file. The failure is structural. Better models make the guess better, but nothing in the codebase records the intent they would need to stop guessing.
How do I stop an AI agent from touching unrelated files?
Instructions help less than people hope. They are advisory, and the model weighs them against its instinct to tidy whatever it reads. The reliable fix is mechanical: give each task an explicit list of files it may touch and reject any change that strays, and check the diff's file list before accepting anything. A boundary that is enforced by machinery does not depend on the model's cooperation.
What is a regression in software development?
A regression is a change that breaks something which previously worked, usually a feature nobody meant to touch. The change itself often looks correct and passes review; the damage surfaces elsewhere, through shared code, and later, when someone finally exercises the broken path. AI-assisted development produces the classic regression pattern at much higher speed.
Will bigger context windows stop AI regressions?
No. A bigger context window lets the model read more of the codebase per change, which improves its inference. But reading is not remembering, and the decisive information is not in the code at all. Why a branch exists, which duplication is deliberate, where a boundary must hold: none of that is written anywhere a model can read, however large the window.
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.