Rescuing an AI-generated codebase without rewriting it
It runs. It reached production. Nobody understands it, and every change breaks something unrelated. This is fixable, and almost never by starting over.
A new category of codebase arrived in the last two years, and it has a distinctive signature. It was built very fast, largely by an AI assistant, by one or two people under pressure. It works — that is the confusing part. It has paying users. And it has reached the point where the team has quietly stopped touching entire directories because changes there break things nobody can predict.
The reflex is to rewrite. Resist it. Rewrites discard years of encoded business rules that exist nowhere except in the code, routinely take three times the estimate, and require you to stop shipping while a competitor does not. In several years of doing this work we have recommended a full rewrite twice.
The recognisable failure modes
Fast-generated code fails differently from ordinary legacy code. Legacy code is usually coherent but dated. This is often modern but incoherent. The specific patterns:
- Near-duplicate components. Five files that are ninety percent identical because each was generated fresh rather than extracted from the last. A bug fixed in one persists in four.
- Error handling that swallows. try/catch blocks that log and continue, so failures surface as mysterious empty states three screens later rather than at the source.
- Tests that assert nothing. Suites with high nominal coverage where the assertions are `expect(result).toBeDefined()`. Coverage numbers become actively misleading.
- Authorisation checked in the interface only. The button is hidden for non-admins; the endpoint it calls is not protected. This is the single most common serious finding in our audits.
- Secrets committed to the repository. Usually in an early commit, still in history long after being removed from the working tree.
- Dependencies that were plausible rather than real — packages hallucinated, then replaced by whatever had a similar name, sometimes unmaintained or worse.
The sequence that works
One: audit before touching anything
Two weeks, read-only. Map the architecture as it actually is. Inventory dependencies and known vulnerabilities. Find every place a secret has ever been committed. Identify the files that appear in the most bug-fix commits — that concentration is where the structural problem lives, and it is measurable rather than a matter of opinion. Output a written document with severity and effort against each finding.
Two: characterisation tests, not unit tests
You cannot safely refactor code whose behaviour is undocumented, and you cannot write specification tests because nobody knows what the specification is. So write tests that pin what it currently does — including behaviour that is arguably wrong. Those tests are not a description of correctness. They are a tripwire that tells you when a refactor changed something, which is exactly what you need.
Characterisation tests turn refactoring from an act of faith into a mechanical process. Everything downstream depends on them existing first.
Three: close the security findings
Before structural work, before performance, before anything cosmetic. Rotate every credential that has ever been in the repository — assume all of them are compromised, because history is public the moment one person clones it. Add authorisation checks at every endpoint and verify them with tests. Patch the dependency vulnerabilities that are actually reachable from your code paths, and record why you deferred the ones that are not.
Four: put a pipeline in front of it
Type checking, lint, tests and dependency scanning on every pull request, blocking merge. This does not fix what is there, but it stops the situation getting worse while you work — which matters, because the team is still shipping features throughout.
Five: then, and only then, structure
Now the duplicated components can be consolidated, the swallowed errors can be surfaced, and boundaries can be drawn between modules that currently reach into each other. Do it in small merged increments behind feature flags. A refactor branch that lives for six weeks is a second rewrite wearing a disguise.
How long, realistically
For a codebase of roughly fifty thousand lines: two weeks to audit, four to six weeks to reach the point where the team can change things confidently again, and ongoing incremental work after that. Considerably less than a rewrite, and you keep shipping the entire time.
The measure of success is not a coverage percentage. It is whether an engineer who joined last month can make a change on a Friday afternoon without anyone being nervous.
Read next
- Healthcare operationsThe hospital systems nobody demosEvery vendor demos the clinical system. Nobody demos the store, the equipment register or the service contract that quietly lapsed.
- InfrastructureWhen on-premise beats cloud, and the costs both sides leave outCloud became the default rather than a decision. There is a specific, identifiable set of workloads where owned hardware is simply better.