Reviewing a Reverse-Engineered Spec Before Rebuilding Legacy Code
Day 4 — Re-engineering: the recovered spec is a defendant, not a blueprint
Point an AI at a legacy system, have it reverse-engineer a spec, forward-build something clean off that spec — and the thing that bites you won't be accuracy. It'll be fidelity. A spec that matches the old system perfectly rebuilds every accident in it, too.
The rewrite that passed every metric and still shipped a bug factory
I've watched a re-engineering job go green across the board and still be a failure. New codebase, modern stack, tests passing, behavioral diff against the old system near-zero. By every number on the dashboard it was a clean win. It was also a faithful reincarnation of a system nobody actually wanted — same weird rounding, same silent retries, same off-by-a-day batch, now with a nicer PR and better test coverage. We didn't rebuild the product. We rebuilt the scar tissue and gave it a fresh coat of paint.
The tell was that everyone celebrated the match. Matching the old behavior was the goal, so matching it exactly looked like success. But in re-engineering, an exact match is the most dangerous outcome you can get — because it means you never made a single decision.
The loop, in one breath
You know the SDD loop by now from Days 1–3: reverse-engineering recovers a spec from what exists, forward-engineering builds from that spec. RE → spec → FE. That's the whole thing. The rest of this lesson lives in the seam between those two arrows, because that seam is where the project is won or lost.
The core problem: an LLM reads behavior, not decisions
Here's what a model actually sees when it reads legacy code. It sees what the code does. It can't see why. It can't tell you which line is load-bearing intent and which is an accident that ossified into place. To the LLM, every one of these looks identical — they're all just behavior the code exhibits:
- ▹A deliberate business rule someone fought for in a meeting three years ago.
- ▹A race-condition patch — a sleep(200) that 'fixed' a flaky integration nobody understood.
- ▹A one-off demand from a client who churned in 2021, still living in a special-case branch.
- ▹A bug that quietly became a feature: users adapted to it, built workflows on it, and now depend on the wrong answer.
The AI surfaces all of it as equal line items. 'When the invoice date is a weekend, the system uses the prior business day.' Is that a requirement, a tax-law constraint, or a bug from a broken date library? The recovered spec states it with the same flat confidence either way. That's the fidelity trap: the model is faithful precisely to the things you most need to question. It has no concept of intent, because intent was never in the code — it was in people's heads, and those people are gone.
Why that's the gift, not the flaw
Now flip it. For the first time, every accidental behavior in the system is written down in one place, in plain language, where a human is forced to look at it. For years that knowledge was scattered — buried in a conditional, remembered by one person, or lost outright. The AI just did the one thing nobody ever had the patience for: it inventoried the entire behavioral surface, including the parts everyone had stopped seeing.
That inventory is worthless as a build target and priceless as an audit. The value isn't that the spec is accurate. The value is that it's complete and legible — it drags the accidents into the light. Your job starts exactly where the AI's ends: it produced the list, you rule on the list.
The gate that decides the project: keep or kill, one line at a time
Take every recovered behavior and put it on trial. Guilty until justified. Each line gets one of three verdicts:
- ▹Intent — a real requirement. Ratify it. It moves into the forward build unchanged.
- ▹Bug-preserved-as-feature — wrong on purpose now. Users depend on it. Keep it, but consciously, with a note saying you chose to, and usually a migration plan to fix it later.
- ▹Dead weight — accident, artifact, or fossil. Repeal it. It doesn't enter the forward build, and you write down why so nobody re-adds it in six months.
This triage is the re-engineering. Not the extraction — the AI did that in an afternoon. The forward build is downstream of these rulings; it's mechanical once the verdicts are in. If your project plan budgets weeks for 'build the new system' and an afternoon for 'review the spec,' you've inverted the actual work. The expensive, senior, judgment-heavy part is the gate. Staff it accordingly.
And this is human work. The LLM can draft a first-pass verdict and flag suspicious patterns — 'this branch handles a customer_id that appears nowhere else,' 'this retry has no backoff, smells like a panic patch.' Useful. But ratify-or-repeal is a business and product decision. Hand the gate to the same model that couldn't tell intent from accident in the first place, and you've just laundered the fidelity trap through a second prompt.
Where characterization tests fit — and why you edit the oracle first
Characterization tests (Michael Feathers' term, from Working Effectively with Legacy Code) pin down what the system actually does by capturing its current outputs as the expected values. They're the honest oracle: not what the system should do — what it does. And here's the modern angle: this is exactly where AI earns its keep. Generating a broad characterization suite by feeding inputs through the old system and snapshotting outputs is tedious, mechanical, high-coverage work. Let the agent grind it out.
But sequence matters, and getting it backwards is a classic self-inflicted wound. You edit the oracle before you enforce it. A characterization test pins a behavior; if you voted to kill that behavior, the test is now defending scar tissue. Enforce the suite as-is and you've hard-wired every accident into your definition of 'passing' — green tests become the mechanism that reincarnates the bugs. The order is: recover → rule keep/kill → delete the tests that pin killed behaviors → then enforce what's left. Your test suite should encode the spec you argued for, not the one you extracted.
WRONG order (how the bug factory ships):
RE spec -> generate characterization tests -> tests GREEN -> forward-build to match
^ every accident now load-bearing
RIGHT order:
RE spec -> KEEP/KILL gate -> prune tests that pin KILLED behavior
-> enforce surviving tests -> forward-build
^ tests defend intent, not scar tissueThe reframe: forward-build the spec you argued against
The spec you carry into the forward build is not the one the AI extracted. It's the one that survived the trial — narrowed by the kills, and deliberately widened where the old constraints were never really requirements. Two kinds of fake constraint hide in every recovered spec:
- ▹Tech-stack artifacts: 'reports run as a nightly batch.' Was that a requirement, or was it because the 2014 database couldn't do real-time? If the constraint was the old stack, it doesn't belong in the new spec. Your AI-era rebuild might do it live — but only if you first recognize 'nightly' as an accident of hardware, not a need.
- ▹Org artifacts: 'orders pass through a manual approval step.' Sometimes that's compliance. Sometimes it exists because two teams didn't talk and bolted a handoff onto the process. Conway's Law fossilized into code. Rebuild it faithfully and you've hard-coded a dead org chart into a fresh system.
Separating requirement from artifact is the highest-leverage move in the whole exercise, and it's the one thing the AI structurally cannot do for you — because in the code, a real requirement and a stack limitation look exactly the same. This is where domain knowledge, not model capability, decides the outcome.
The discipline for Monday
Treat every recovered behavior as guilty until justified. The AI's spec is a brief for the prosecution, not a set of blueprints to execute. Budget the project around the keep/kill rulings — that's where the senior time, the domain arguments, and the real risk live. The extraction is cheap now; the judgment is the job. Skip the gate and forward-build on fidelity, and you'll pass every test and ship the same broken system with a better commit history. Green doesn't mean right. It means faithful — and faithful is the trap.
Extend your knowledge
- ▹Read Michael Feathers, Working Effectively with Legacy Code — specifically the chapters on characterization tests and finding seams. It predates AI but the mental model (pin behavior first, understand it, then change it safely) is exactly what the keep/kill gate needs.
- ▹Look up the Strangler Fig pattern (Martin Fowler's writing on it). Once you've ruled keep/kill, you rarely want a big-bang forward build — you want to route behavior incrementally so each ratified/repealed decision ships and gets validated on its own.
- ▹Read up on Conway's Law and the 'inverse Conway maneuver.' It sharpens your eye for org artifacts — process steps that exist only because of team boundaries, not requirements — which are the hardest fake constraints to spot in a recovered spec.
- ▹Try it small: point an agent at one gnarly module you own, have it reverse-engineer a spec, then personally rule keep/kill on every line. Count how many you kill. That ratio is your evidence for how much of the project's real work lives in the gate, not the extraction.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.