Engineering case study

Taking over a system nobody can explain any more

A working production system arrives as a folder. Its authors are gone, its specification never existed. This is an account of coming to understand it, rebuilding it, and handing it back in a form the receiving party can stand up and verify alone.

Document v1.1.2 Figures measured 2026-09-07 Written by Claude Code Magyarul

01In summary

An insurer's product-management and policy-sales system — an AngularJS front end on a PHP back end, embedded into a public web page — was handed over for modernisation. Two developers, AI-assisted, over 234 days of which 96 carry commits. The work produced 125,769 lines of source and 991,137 lines of documentation.

Read those last two numbers again. Roughly eight lines of documentation for every line of source. On a project that builds something new, a ratio like that would be absurd. Here it is the shape of the work.

What the client was buying was not code. They already had code — working, in production, for years. What they lacked, and what had stopped them changing anything, was understanding. And understanding has no other form: it exists as documentation or it does not exist at all. In a takeover the documentation is the deliverable, and the ratio is simply what that costs.

Before any of that there was an act nobody plans for. The inherited system was not containerised, and no deployment documentation existed in any form — so the first work was not understanding it but making it reproducible: standing it up from scratch, by experiment, until it could be observed twice and give the same answer.

Then three acts, and most legacy projects fail by skipping one of them. Come to know the system and prove that you know it. Rebuild it so the rebuild is verifiable against the original rather than merely nicer. Then hand it back so the receiving party can run and check it without you — which is a distinct engineering job, not the moment after the last commit.

What is not here

Nothing from the client's system appears on this page: no screen, no schema object, no business rule, no commercial figure. The captures taken while documenting the original are counted here — 539 of them — and shown nowhere. The client is not named, and neither is their market segment.

02What was inherited

The starting position was a folder. In it: the legacy database with its structure and its data, the original application source for both halves — an AngularJS single-page front end and a PHP back end, served inside a WordPress shell page — the initialisation and seed scripts, and the pricing tables in two formats.

What was not in it matters more. The system was not containerised in any form, and there was no deployment documentation whatsoever — nothing describing how to stand it up, what it depended on, or how its environment was configured. A running production system existed somewhere; a way to reproduce it did not.

The act nobody plans for

Before a single behaviour could be documented, the inherited system had to be made reproducible. That meant containerising it from scratch and working out its environment, its character encoding and its initialisation by experiment — roughly 400 files of environment work that produced no documentation and no product.

It is the easiest thing in a takeover to leave out of a plan, and everything else is impossible without it. You cannot document behaviour you cannot observe twice.

Even with all of that, it was still not a specification. A running system plus its source tells you what happens; it does not tell you what is supposed to happen, and it certainly does not tell you which of its behaviours are decisions and which are accidents that nobody has dared touch.

The data model underneath was layered: reference data at the base, the product structure above it, then the product-dependent configuration where pricing and surcharge rules live, and the business modules on top. That layering is the reason a system like this becomes untouchable. A change to a rule near the bottom reaches the surface through paths nobody has mapped, so eventually nobody changes anything, and the system quietly sets the pace of the business rather than the other way round.

That is one cost of a system nobody can explain. Not the maintenance bill — the decisions that stop being taken.

But that is not why a modernisation gets funded

Discomfort does not release a budget. What does is the moment the stack itself becomes disqualifying: when the business is held to a requirement the system cannot meet at all, rather than one it meets awkwardly.

The categories that turn "we should modernise this eventually" into a funded project. Which of these applied, and to what degree, is the client's business and is not described here.
What the old stack could not satisfy
Runtime and framework versions past their supported life
Security and compliance obligations the stack cannot meet
Deployability — nothing containerised, nothing reproducible
Integration with systems that expect current interfaces
A shrinking pool of people willing to maintain it

These are not preferences. An unsupported runtime is not "old"; it is a component that will never receive another security fix, on a system holding other people's data. That is the argument that gets signed off, and it is worth stating plainly, because a modernisation proposal built on developer comfort deserves to lose to one built on this.

03The question a takeover actually asks

"Can AI write this faster" is the wrong question, and it is the one most accounts answer. Speed at writing code is not the constraint in a takeover. On day one there is nothing safe to write, because nothing is understood well enough for a change to be provably harmless.

The real question has two halves, and the second is the hard one:

How do you come to know a system nobody can explain any more — and how do you prove that you know it, to yourself and to the person paying?

Proof matters because understanding is invisible. A developer who has read the code feels confident, and that feeling is indistinguishable from the same feeling held by a developer who has misread it. The only difference that survives contact with reality is whether the understanding was written down in a form somebody else can check.

Which is why act one produced documents rather than code, and why the documents were audited rather than merely written.

04Act one: understanding, as a deliverable

Reverse engineering was run as a product, not as preparation. Each area had a definite scope and was tracked to completion, and several carried an average audit score rather than a tick.

Three acts: understand, rebuild, hand over 1 · Understand document it, audit the documentation, prove it 2 · Rebuild core first, then one-to-one, against the original 3 · Hand over packaged, configured, verifiable without you skipped → you rewrite bugs you never knew were features skipped → nothing to verify the rebuild against skipped → the client owns code they cannot run the failure mode of each act is the act below it
Most legacy projects skip act one because it produces no running software, or act three because the code "is finished". Each omission has a specific, predictable cost.
Act one, tracked to completion. The counts are as the project recorded them; the content of every one of these documents stays private.
AreaDocumented
Database table groups32 of 32
Separate database audits33
Screen documents142 of 142
Function documents25 of 25
Front-end documents25 of 25
Back-end documents14 of 14

Behaviour was captured as well as described. Where a written description of a screen could be read two ways, a capture from the running original settles it. Those captures are evidence for the person doing the rebuild, not illustrations for a reader — which is why this page counts them and shows none.

The ratio is the honest measure of a takeover. A project that reports a thin documentation trail after inheriting an undocumented system has not done act one; it has skipped to act two and is about to reimplement behaviours it never understood, including the accidental ones.

05Act two: rebuilding

Rebuilding began with the pricing engine, described in the project's own plan as the heart of the system. Building the core first, standalone, means the part everything else depends on is finished and testable before anything is built on top of it. It also concentrates the hardest domain logic into a component small enough to be reasoned about.

Then the two halves — and here is the mechanism that makes the whole rebuild verifiable, and the most transferable thing in this account.

The test suite was written first, against the still-running original. It is the invariant. Each half was then replaced on its own, and every resulting configuration was measured against the same unchanged suite.

The migration matrix: one test suite, four configurations, two independent swaps ONE TEST SUITE — authored against the original Origin AngularJS on PHP Intermediate AngularJS on Node Intermediate React on PHP Target React on Node swap the back end swap the back end swap the front end swap the front end Constant throughout the database, never migrated · a payment-provider emulator · a mail catcher instead of real SMTP one variable at a time — a red configuration has one candidate cause, not two
All four configurations were built and tested, not just the two ends. Each swap could therefore be verified on its own, by either route, and both intermediate configurations are systems that run and pass — not scaffolding.

The original ran AngularJS on PHP, inside a WordPress shell page. The target runs React on Node, inside the same shell. Both intermediate configurations were built and tested too — AngularJS on Node, and React on PHP — which is what turns a leap into two separate, verifiable steps. The rule is simply: never swap both halves at once. When a configuration goes red, there is one candidate cause, not two.

Neither intermediate is scaffolding. Each runs, passes the suite, and could have been shipped as it stood — which is what makes it safe to stop part-way, and on a client project that is not a theoretical property. Budgets and priorities move.

What had to stay still

Swapping one variable at a time only means something if everything else is genuinely constant. Three things were held fixed for the whole route: the database, which was never migrated; a payment-provider emulator standing in for the real payment service; and a mail catcher instead of a live SMTP server.

The last two are not conveniences. A test that reaches a real payment provider is not repeatable and should not be run at all; a test that sends real e-mail is worse. Replacing every external dependency with something deterministic is the second half of act zero, and it is the reason the suite could run unattended, in any environment, as often as needed.

Why the suite has to come first

A rewrite normally has no oracle. Every difference between old and new is either an improvement or a regression, and there is no way to tell which — the knowledge that would settle it is exactly what you did not have when you started.

Authoring the suite against the original creates that oracle while the original is still there to be asked. It is a wasting asset: the day the original is switched off, the answer to "what was it supposed to do" becomes unavailable for ever. Write the questions down while something can still answer them.

The rebuild was planned before it was written, in numbered phases with an audit score each: 17 phases for the first engine generation, 13 for the second, and 15 fix plans between them. Separately, a full system plan for the eventual complete rewrite was produced and audited — 16 documents, 13 phases, 5 milestones, with a target stack of a modern server framework, a React-based front end and PostgreSQL.

Naming a target stack identifies nobody and is the part a reader can use. The plan's commercial figures belong to the client and are not reproduced here.

06Act three: handover as engineering

This is the act most projects treat as an afternoon of copying files, and the one that decides whether any of the rest was worth doing.

Handover is not publishing. It is the receiving party being able to stand the system up, in their own environment, and verify it themselves — without the person who built it in the room.

That has a shape, and the shape costs real work.

What handover actually consisted of. Each package carries its own container definition, environment template, test scripts and pipeline; the packages then had to stand a whole system up, in an environment other than the one they were built in.
ArtefactCount
Independently deliverable packages8
Stack configurations built and tested4
Acceptance rounds, each a full pass5
Handover issues139
Handover implementation plans92
Environment configuration files44
Handover test scripts52
Continuous-integration pipeline files23
Handover concepts and reports54
Handover: eight packages verified separately, then three stacks in two environments Eight packages, each verified on its own every one with its own container definition, environment template, tests and pipeline database original front end original back end shell page new front end shell + React new back end shell + React + Node Then acceptance across three stacks, in two environments the same suite, run end to end against a fully stood-up system AngularJS on PHP React on PHP React on Node AngularJS on PHP React on PHP React on Node environment A environment B eight component verifications, then six full-stack acceptance runs a release counted as done only when every layer passed in every one of them
The first row is what gets handed over; the second is what proves it works. Verifying a package on its own says only that it starts. Standing up a whole system from those packages, in an environment that is not the one it was built on, is what the receiving party will actually have to do — so that is what was tested, twice over.

And the handover closed where it should: merged into the client's own version control and tagged there. Not a zip file, not a shared drive. The work landed in the system the receiving party already uses, in a state they can inspect, revert and continue.

07The correction loop

Two streams of findings ran, and they ended in different places. That difference is worth being precise about, because the honest version is more useful than a clean one.

Findings reported from outside — by the people testing what was delivered — were all fixed, and all reported back. That loop is closed. A defect that is fixed but never confirmed to the person who raised it is not closed; it is merely quiet.

Findings from the project's own internal testing are not all fixed. Some remain open at the point this account was written. Saying so costs nothing and buys the only thing that matters in a report like this, which is that the other statements can be believed.

The correction loop, closing in the client's own version control Finding raised written up, prioritised Failing test first reproduce, then fix Separate round revalidates the fix Client’s own repository merged and tagged there reported back to whoever raised it — the loop is not closed until they know a fix is not the end of the process; the revalidation round is
The fix is the middle of the loop, not the end. A separate, later round re-examines what was fixed and reports how many findings remain open.

The audit rounds show the shape. On the rebuilt back end, an initial round examined 50 candidate findings and confirmed and fixed 17 — meaning two thirds of what was raised turned out not to be defects. A later revalidation round re-examined those 17 and closed them with 0 still open. The front end went through four such rounds.

Separately, one coverage campaign ran the end-to-end suite 604 times across 12 environment configurations and surfaced 12 issues. A suite that only ever runs in one environment cannot find that class of defect at all.

08The architecture, and why it changed

The work ran on two machines, with a deliberate line between them.

Two machines: build and test on one, make deliverable on the other Machine 1 develops and tests reverse engineering · the rebuild unit and end-to-end tests work fans out to subagents inside each session Machine 2 prepares, assures, delivers packaging · environment configurations acceptance rounds · the handover itself the same discipline, a different responsibility built the party that builds never certifies its own work — an ordinary engineering rule, applied to an unusual pair of jobs
One machine builds and tests. The other makes the result deliverable, assures it and hands it over. Neither machine grades its own homework, which is the whole reason the line is drawn where it is.

Inside a session, work was decomposed to purpose-briefed subagents, one per task. What did not exist was a set of standing role sessions coordinating with one another, each holding a fixed responsibility over time.

The constraint was the tool, not the practitioner

That was not a preference. At the time this project ran, agents could not coordinate with each other without a human standing in the middle as the message bus — and a human message bus does not scale past about three of them. Until that changed, the achievable unit was machine-level separation plus subagents inside sessions, which is exactly what was built.

The ambition was never the limiting factor. The tooling was.

This account is therefore a dated snapshot, not a claim about what is possible in general. The project began in November 2025, and what an arrangement like this can reach has moved since. Check the measurement date before drawing any conclusion from it.

The practical lesson generalises past this particular toolchain: when a method seems stuck, the binding constraint is often not the discipline but what the tools let you coordinate. It is worth knowing which one you are up against, because only one of them improves by trying harder.

09The time axis

First commit 2025-11-20, last 2026-07-12: 234 elapsed days, of which 96 carry commits, across 482 commits and 28 tagged releases.

Commits per month, including the two dormant months 142 0 Nov Dec Jan Feb Mar Apr May Jun Jul dormant January carries a single commit and February none — kept in the figures rather than smoothed away
The curve is not smooth, and the gap is not an embarrassment. Client projects wait — for decisions, for access, for the client's own priorities. A case study that hides its gaps is describing a project that never existed.

The two dormant months matter for a reader trying to plan. Elapsed time in client work is not effort; it is effort plus the client's availability, and the second is usually the larger term. Quoting 234 days as if it were 234 days of work would be a lie by arithmetic.

10What was measured

Kept short: this is evidence for the account, not the account.

Version-controlled content in the project workspace, measured 2026-09-07. Source includes the inherited system's own code, which lives in the workspace; separating them would flatter the output. Line counts are physical lines.
MeasureFilesLines
Documentation2,490991,137
Source644125,769
Tests22350,262
Database scripts4312,152
Shell automation154—
Behaviour captures (never shown)539—
All tracked files4,597—

The documentation splits roughly into the rebuild specifications, the reverse-engineering record of the original system, and the handover material — with the handover alone accounting for several hundred documents.

11How correctness was established

Test results as the project last recorded them. The partial row is reported as it stands.
SuiteResult
Rewritten front end, unit96 / 96
Ported back end, unit67 / 67
Policy wizard, end to end8 / 8
Validation suite20 / 20
Authentication flow, end to end47 / 56

These suites are not incidental. The end-to-end tests are the same ones authored against the original AngularJS-and-PHP system, unchanged — which is what makes a number like 8 of 8 mean something. It is not "the new code passes its own tests"; it is "the new code answers the questions the old system answered, the same way".

The last row is the interesting one. It is reported here because it was reported there — in the project's own status line, at the top of its own README, where anyone opening the workspace would see it first. A team that puts its worst number where it cannot be missed is a team whose good numbers are worth something.

Correctness rested on three legs rather than one: unit tests around the rebuilt components, end-to-end runs through the real flows, and the audit rounds that examined the result independently of whoever produced it — including the two-thirds of raised findings that turned out not to be defects at all.

The gain nobody puts in the budget

The inherited system arrived with no automated tests at all. It now has 50,262 lines of them across 223 files.

Nobody bought that. It arrived as a by-product of needing an oracle — and because the suite was authored against the original, it describes the behaviour the business actually depends on rather than the behaviour of the rewrite. It outlives every implementation it was used to verify, including the ones still to come.

12What did not get finished

The project stopped at a phase boundary, and the record says so plainly.

Understanding, the minimum viable core, the rebuild of the front end and the port of the back end were completed, as was the full system plan for the eventual complete rewrite. The implementation of that plan and the data migration behind it are recorded as planned, not as done. They were never started.

On defects, the position is asymmetric and worth stating exactly:

Where the two streams stand

Reported from outside: all fixed, all reported back. That stream is closed.

Found by internal testing: not all fixed. Some remain open.

The asymmetry is a choice, not an oversight. A finding raised by the people relying on the delivery blocks their work and closes the loop only when they are told it is done. A finding the team raises against itself competes for the same hours as everything else, and some of them lose. Reporting the two as one number would hide exactly the distinction a reader needs.

None of this is a disclaimer. A project that can say precisely which acts it completed, which it planned, and which findings it is still carrying is demonstrating the same discipline as one that can prove what it shipped.

13What transfers to your legacy system

Little of this is specific to insurance, or to this stack. Four things transfer.

The sequence, and its failure modes

Understand, rebuild, hand over — in that order. Skip act one and you will faithfully reimplement bugs you never recognised as bugs, because from the inside an accident and a decision look identical. Skip act three and the client owns code they cannot run, which from their side is indistinguishable from owning nothing.

The ratio to budget for

Expect documentation on the order of the source, and in a takeover expect it to exceed the source substantially — here by roughly eight to one. If your plan has a line for "documentation" at five per cent of effort, your plan is for a greenfield project wearing a takeover's name.

The handover checklist

Can the receiving party stand it up in their own environment, from what you gave them, with you unavailable? Does each package carry its own configuration template and its own tests? Have you proven it in more than one combination? Does the work land in their version control, tagged? If any answer is no, the handover is a hope, not an engineering artefact.

The verification oracle, and how to bank it

While the original still runs, you have something no greenfield project ever has: a reference implementation that answers every question about intended behaviour. It is a wasting asset.

So bank it. Write the test suite against the original before you change anything, then replace one half of the system at a time and measure every intermediate state against that same suite. Two independent swaps instead of one simultaneous leap; a single candidate cause whenever something breaks; and a contract that survives every implementation you throw away.

If you take one thing from this account, take that. It costs a fortnight at the start and removes the question "is this different or is this broken" from the whole rest of the project.

14What it costs, and where the method stops

The cost

Roughly eight lines of documentation per line of source is not a rounding error in a budget. It is most of the budget. Anyone adopting this approach should price act one honestly and expect to defend it, because it is the act that produces no running software and is therefore the easiest to cut — and cutting it is what turns a modernisation into a very expensive rewrite of misunderstandings.

Why it is not a cost you pay once

Whether AI-assisted work is effective on a system does not depend on the model. It depends on three properties of the system itself:

The same three conditions decide whether a human joining the project can be productive in a week. Nothing about them is specific to AI; AI merely makes their absence impossible to bluff past.
ConditionInheritedRebuilt
A reproducible environment — it can be stood up on demand and behaves the same waynoyes
Written understanding — what it does exists as documents, not only in a headnoyes
Behaviour locked in tests — a change can be shown safe, not argued safenoyes

The inherited system had none of the three. The rebuilt one has all three — not as a goal anybody set, but as a consequence of doing the three acts properly.

Which answers the question the ratio raises. The documentation is not a one-off cost recovered by the delivery. It is the substrate that makes every later change cheap — whoever, or whatever, makes it.

That is the part worth understanding before deciding whether act one is affordable. A system that arrives undocumented, untested and unreproducible is expensive to change for ever, and every year of deferral compounds it. The same system with those three properties can be maintained and extended the same way it was rebuilt, at the same speed, by whoever comes next. The modernisation is the visible deliverable. The change in what the system costs to change is the durable one.

Where it stops

The method establishes what a system does and rebuilds it verifiably. It does not decide whether the business still wants that behaviour. Every faithfully reproduced rule is also a faithfully reproduced decision from years ago, and some of those deserve to be questioned rather than ported. That conversation is the client's, and it belongs after act two, when there is finally something measurable to change.

Nor does it remove judgement. Two thirds of the findings raised in the first audit round were not defects. Someone had to work out which third was real.

If you are sitting on one of these

Most companies do not have a greenfield project. They have a system that works, that nobody fully understands, and that quietly decides what the business can and cannot do next. If that is familiar — or if you have taken one of these on and would tell it differently — I am interested in the comparison. Ferenc Erb on LinkedIn

Who did this

Two developers: Ferenc Erb and László Villányi.