Back to News & Insights
Artificial Intelligence September 17, 2026 · 6 min read

Show a model your old code and it writes your old bugs: 32 runs, 0% reuse

Same task, same models, two commits of my own repo. From the pre-migration commit every run hand-rolled a 190-line typeahead with the exact defects the migration removed; from the post-migration commit every run reused the shared component in 41 lines.

Show a model your old code and it writes your old bugs: 32 runs, 0% reuse

Last July I spent seven pull requests deleting the same component eleven times. Eleven games in my football quiz app had each grown their own search box, and they had drifted apart in the way duplicated code always does: arrow keys behaved four different ways, only four of the eleven kept the mobile keyboard down at the start of a round, several could scroll the page while you were arrowing through results, and one could submit a stale result. The migration replaced all of them with a single shared component, and the final pull request added a test that fails if a twelfth one ever appears.

That gives me something most people writing about AI and code do not have: a repository where I know exactly what the right answer is, and where the wrong answer used to live at a known commit. So I asked a question I had been wondering about since I started letting models write parts of this app. Does the code that already exists decide what the model writes next?

The setup is deliberately boring, because the whole point is that only one thing moves. I wrote out a task asking for a footballer search box on a new game screen, told the model to match the conventions of the code it was being shown, and then sent that same text over and over, eight times per model per condition, at a temperature of 0.7 so the runs would not all be carbon copies of each other.

What changes between conditions is which commit of my own repository gets pasted in above the task. For the first I went back to 82385a5, where nine per-game search components are sitting there and nothing shared exists yet, and for the second I used cc78828, the commit that closed the migration, where the shared component is present and TeamTies is shown calling it. Both conditions get roughly four hundred lines of genuine source from the repository as it stood, so neither is winning on prompt size, which was the first objection I expected and wanted to rule out before I looked at anything else.

The models were DeepSeek v4 Pro, Llama 4 Maverick, GLM 5.3 and Mistral 3 14B, an odd-looking lineup that has a dull explanation: Claude and GPT both come back 403 on my account's tier, so open weights are what I had to work with.

| context shown | reused the shared search | hand-rolled a new one | median lines | |---|---|---|---| | the forked era | 0% | 100% | 190 | | after the migration | 100% | 0% | 41 |

Thirty-two runs each, and not one crossed over. Same model, same instructions, 190 lines of hand-written state machine in one case and a 41-line wrapper in the other, decided entirely by what was lying around in the repository.

I expected a tendency. I did not expect it to be unanimous across four models from four different labs.

The interesting part is not the line count, it is what is in those 190 lines. The migration existed because eleven copies had drifted on specific behaviours, and the test I added lists them. So I checked the generated components against that same list.

Of the 33 hand-rolled components the old context produced: 0% made the arrow keys wrap around at the ends of the list, which is the first behaviour the drift guard names. 0% used SEARCHMINQUERY_LENGTH, and 97% re-typed a bare >= 2 instead — the exact thing the repository's own documentation tells you never to do. 58% had any mobile focus guard at all, against a rule that says the keyboard must not cover the board until the player has had a go.

These are not generic AI mistakes. They are my team's mistakes, from 2025, faithfully reproduced in 2026 by models that were shown the code containing them. The forked components taught the models to fork, and taught them the defects too.

There is a cleaner way to put the damage. Every one of the 32 components generated from the old context calls useFootballerSearch. That hook was deleted in the final pull request of the migration. All 32 would fail to compile against the repository as it stands today, which is a strange thing to say about code that perfectly matches the codebase it was shown.

If old code drags the model backwards, the practical question is what you can do about it when your repository is full of old code. I ran two more conditions to find out.

In the first, the model sees only the forked components, exactly as before, plus one paragraph from the repository's CLAUDE.md: the rule saying every search goes through the shared utility and never hand-roll another typeahead. In the second, the model sees no source for the shared component at all, only its filename in a directory listing.

| context shown | reached for the shared search | hand-rolled | |---|---|---| | forked code only | 0% | 100% | | forked code + the written rule | 100% | 0% | | just the filename in a listing | 97% | 3% | | the shared component's source | 100% | 0% |

One paragraph of documentation, sitting next to nine hundred lines of exactly the code it forbids, flipped every single run. That is a better return than I expected from a file most people assume gets skimmed.

Here is where the neat story breaks, and it took a second measurement to see it.

Reaching for the shared search only means the model wrote the component's name into its JSX. It does not mean it called it correctly. The component has 23 real props, so I compared the props in each generated call against that list.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation