Legacy migration is a different sport
Every AI coding demo I've seen starts from an empty folder. Describe the app, watch it appear, ship it. The work I've been doing for the last stretch is the opposite: a fifteen-year-old WebForms application that handles tens of thousands of updates a day, being rewritten in ASP.NET Core, with real clients, real payments and real integrations pointed at it the whole time. These are field notes from that. Part 1 is about verification. Part 2 is about cutting over safely. Part 3 is about the discipline.
The behaviour was never written down. It grew, one hotfix at a time, for fifteen years. Nobody who built the first version is still around, and the people who maintain it now know it the way you know a house you've lived in for a long time: by habit, not by blueprint.
That makes the most basic question in a rewrite unanswerable. "Does it work?" has no answer without a ground truth to compare against. There is no document that says what the system should do. There is no test suite that encodes what it does do. There is only the thing itself, running, serving traffic, being right in ways nobody has catalogued.
Greenfield doesn't have this problem. The spec is whatever you decide it is. Legacy migration has nothing but this problem.
If the truth lives in production, go and get it from production. Take a day of real logs. Replay every event against the old app and the new one, at the same time. Pair the responses. Diff them.
The old system becomes the spec. Every request it ever served is a test case you didn't have to write, with an expected answer you didn't have to invent. Four moving parts:
The important property is what isn't in the picture. There are no mocks at the edges. The harness talks to the real legacy app and the real rewrite, with real traffic, and records what actually came back.
No mocks on the outside. Real traffic, real systems, real answers.
One mismatch survived the diff. The two databases disagreed on status values for the same records. It looked like a bug in the rewrite, and for an hour it was treated as one.
Investigation traced it to a documented enum convention difference between the two systems. The legacy app stored one representation, the new one stored another, and both were correct by their own rules. The harness found it. The follow-up cleared it.
That is the shape of the tool. It does not decide whether a divergence matters. It makes sure you can't miss one.
That's the harness working: it flags, you judge.
The harness above is the version that worked. An earlier version did not, and the way it failed is the real subject of this post.
To replay traffic, the harness had to speak the legacy wire format: parse the log lines, reconstruct the payloads, post them the way the old clients post them. I described the system to the AI, in reasonable detail, from memory. The AI wrote the contract from the description.
"ER", "COMPLETE"Nothing on the left is absurd. It is exactly what a sensible engineer would design if asked to build this system today. Which is the problem: nobody asked it to design the system. They asked it to match one.
And the smoke tests passed. Every one. They ran against stubs built from the same fiction as the code under test. The tests and the code shared an author, and the author had never seen the real system. Green across the board, and not a single assertion that touched reality.
A passing suite proves consistency, not correctness.
The instinct is to write a more careful description. More detail, more caveats, more "make sure you check". That would have produced a more detailed fiction.
The fix was uploading the real legacy source and making the AI read it before writing a line. Not a summary of it. The code. The invented contract fell apart on contact with the actual implementation: the segment count, the status codes, the phantom enum, the transport. All of it caught in one pass, because for the first time there was something real to be wrong against.
From then on, the rule: the model never describes the legacy system from memory. It reads it, or it doesn't speak. Every later piece of the harness, and every pattern in Part 2, starts with the real source in context.
Greenfield has nothing to hallucinate against. Legacy does.
In legacy work, the AI's biggest risk isn't bad code. It's coherent code written against a system that exists only in its head. It compiles, it's tested, it's wrong, and nothing inside the loop can tell you.
Verification has to come from production. Real logs, real source, real replays. Never from the model's own account of the system, and never from tests the model wrote to match that account.
This is why legacy migration is a different sport. The greenfield demo is impressive because the model is free to make things up, and making things up is what it does best. The legacy job is hard for exactly the same reason.
The old system is the spec. Make the AI read it.