4.1 Brownfield
You have the mindset, the governed environment, and the models; this Part puts them to work, and it starts where most readers actually stand.
Everything so far has assumed a clean start. Greenfield is the easy case: sketch the new thing, have an agent vibe-code a first prototype, and work your way down into an increasingly governed environment, avoiding the mistakes your last project taught you. Most readers, of course, are not on a clean start. Most are staring at brownfield — a legacy codebase, started before agents existed, and since made worse by an agent that added a hundred thousand lines nobody can read.
That codebase comes wrapped in documentation: tests of uneven quality, an architecture known mostly by reputation, and a body of writing everyone consults but nobody quite trusts. Usually that writing is a wiki. It matters because of the distance involved. The path from code to a complete executable model is long. The path from code to a better wiki is short. The wiki already exists, engineers already know how to use it, and everyone already agrees it should describe the system more accurately than it does. So most engineering organizations already own the beginnings of a governed engineering environment, one where engineering knowledge and policy live in shared models, tools, and checks instead of in a few people's memory. They just call it the wiki. This chapter starts there. It turns a drifted wiki into a linked and increasingly trusted picture of the system, then carries that picture toward the stronger models and mechanisms the earlier Parts built.
Neither high deployment velocity nor the risk that rides with it is new. The industry learned years ago that shipping to production hundreds of times a day is possible, and that it is unforgiving when the discipline behind it slips. High velocity, high risk: just ask Amazon, which was deploying to production every 11.6 seconds a decade before an agent could write the code for you.
MAGE rests on two moves. The Modeling Thesis makes intent and system structure explicit in representations an agent can reason through. The Alignment Thesis turns important obligations into mechanisms the environment applies to later work. Brownfield adoption runs both moves incrementally over the artifacts an organization already has: first make the map more explicit, then add the checks that keep the map and the code from drifting apart.
Where this chapter fits
MAGE separates two concerns.
- Models give an agent a compact representation of what the system is and what it should do.
- Mechanisms keep the implementation aligned with those representations.
This chapter builds both from what you already have. The wiki is an accessible first model; links and audits give it traceability; lints and completion checks hold the repaired relationship in place; selected high-value claims later graduate into structured, executable models.
4.1.1 Start with the model you already have
Do not dismiss a drifted wiki as failed documentation. Treat it as an immature model — a cheaper representation of the system that keeps the structure some engineering purpose needs and drops the rest.** Recall that a model here need not be formal or executable — a diagram, a wiki, or a prose spec all qualify, as long as the properties it claims to represent are clear. A wiki qualifies. It is already a lightweight knowledge graph: its pages are nodes, its links and backlinks are edges. Its tags and categories carry loose metadata, and its search makes all of it navigable. The semantics are weak, but the graph is already there.
What the wiki lacks is a disciplined join to the territory. A page describes a service but never names the code that implements it. A test guards an invariant but never links the decision that says why the invariant matters. A subsystem runs in code with no explaining page at all. The graph floats beside the repository instead of joining it.
So the first brownfield move is not to replace the wiki. It is to connect it. Establish traceability as a round trip: the important wiki claims point down to the code and tests that realize them, and the important code and test regions point back up to the pages that explain their purpose. This is traceability in the book's specific sense — a two-way join, not a heap of hyperlinks.†† The earlier Parts develop the stronger form of this join: every model node points to the code it governs, and that code points back to the model node that explains it. Figure 4.1-1 draws the join, and draws why the wiki you have and the model you want are the same shape at different strengths.
Once the links run both ways, an audit can sort every gap into one of three cases:
- a missing page or model;
- a missing anchor to a page that already exists;
- code that sits legitimately below the representation's grain — too fine or too generic to earn a page of its own.
The distinction keeps the audit honest. The goal is not one wiki entry per function. The goal is a map whose claimed surface agrees with the territory it represents.
The minimum package
Four small additions turn the wiki from optional documentation into the start of an engineering environment.
- Bidirectional links. Wiki pages name the code and tests that back their claims; code and tests name the pages that explain their purpose.
- Agent briefs. Agents are told how to reach the wiki, how to walk its links and tags, and when a page's claims should shape the task in front of them.
- Design templates. New work names the wiki pages it touches, and records when a missing page has to be created.
- Definition-of-Done checks. Before work is accepted, the affected pages are read against the current repository, not trusted because an agent reports it updated them.
One rule governs all four: never add metadata without adding consumers. A tag, a backlink, an ownership field, a machine-readable mirror — each earns its place only when an audit, a brief, a retrieval step, an analysis, or a gate reads it. Metadata with no reader is one more surface free to drift.
Audit, synchronize, govern, extend
The migration runs in four stages. Each one reaches a useful stopping point, so an organization does not have to arrive at executable models before the work starts paying for itself.
Audit. Spend a bounded set of agent sessions building the graph. Inventory the major wiki regions, find the code and tests each one claims to describe, add the links in both directions, and create or propose pages for the large implementation regions with no explaining home. The exit criterion is coverage of the surface that matters: every major wiki region links to the code and tests that realize it, and every major code and test region is linked, marked below the wiki's grain, or recorded as owing a new entry. This is an audit, not a gate. The wiki is known to drift; making it mandatory before repairing it would only promote old errors to official instructions.
Synchronize. Once the graph exists, drain the drift with ordinary feature and maintenance work. An agent that touches linked code or tests reads the attached pages, checks their claims against the implementation, fixes the nearby factual errors, adds the missing links, and flags the disagreements that need a human to settle. This is a boy-scout rule, not a whole-wiki rewrite: leave the linked surface no less accurate than you found it. The exit criterion is a fresh audit that shows the important wiki claims and the repository substantially in agreement, with no major orphan left unclassified.
Govern. After the wiki earns trust, promote it from optional context to required input. Agent briefs now call for task-specific wiki analysis before implementation. Design templates name the pages the work should touch. The Definition of Done requires the relevant claims to be checked against the repository at HEAD. The promotion order is the one this chapter later applies to lints: audit first, drain the findings, then make it mandatory. This is the Alignment Thesis applied one increment at a time — an obligation that used to depend on someone remembering to update the wiki becomes part of the environment's acceptance criteria.
Extend. A trusted wiki can stay a human-facing model for as long as that serves. Where the value justifies the cost, selected pages go further. Add structured correctness criteria, invariants, dependencies, ownership, allowed transitions, evidence obligations, and links to the validators or tests that discharge them. Mirror those fields in a machine-readable form once a real consumer exists. The data can then drive task-specific context retrieval, drift analysis, test-obligation derivation, semantic lints, or admission gates. The exit criterion is not "the wiki holds structured metadata." It is sharper: every structured field added during extension drives at least one retrieval path, analysis, generator, validator, or gate.
4.1.2 The starting point and the destinations
Name the starting point and both destinations before you move.
The starting point is familiar: some spaghetti, a drifted wiki, thin or uneven tests, and invariants that live only in one person's head — the person you hope never retires.
The first destination is a trusted map. The major wiki claims link to the code and tests that realize them; the major implementation regions link back to the pages that explain their purpose; agents read and maintain those joins during ordinary work. You reach it in weeks, and it pays off long before the far destination arrives.
The far destination is the full model-based environment the earlier Parts built. You have the models needed to express every view of the system that bears on a quality you care about — performance, correctness, security, privacy. You want code that reflects those models, so the map equals the territory — the code agrees with every property the model claims to represent. Zoom in on the map and you see county lines, not Switzerland. And you want traceability for your quality case, the ability to state the argument automatically. For any property you claim, you can point to the code that implements it, the static analysis that enforces it, the protocol you model-check it against, the tests that confirm it across the inputs you try, and the record of every change and why it was made. With that in hand, soft and hard governance hold the whole thing in place and keep it current.
The wiki path does not replace the three migration approaches that follow. It gives them somewhere to meet.
- Top-down work contributes the intended architecture, the requirements, and the correctness claims.
- Bottom-up induction finds the structure the implementation actually holds.
- Lint and coverage make the code legible and safe enough for the two to converge.
The wiki is the first shared surface where those views can be compared. Some organizations stop at a trusted, linked, human-facing map. Others keep going, pulling the highest-value claims into typed, executable, drift-gated models. Either way, a real migration uses all three approaches, so take them one at a time.
4.1.3 Approach one: top-down, from a whiteboard
Start coarse and refine. Your first model can be as crude as "on this input, the test suite passes and we get the right output" — and you should be able to write that today. From there you ask what happens between input and output: insert a file, and out comes a score, a spreadsheet, whatever your program produces; insert a picture, and out come the cats and antelopes it found. Refine until the model captures the abstractions you need, and stop when you hit diminishing returns — the territory is the territory, and the map only has to be detailed enough to carry the assurance you are after.
You can seed this from a literal whiteboard: photograph the diagram, hand it to the agent, and have it produce a data-flow diagram in whatever representation you like. Then iterate against the code — "what nodes am I missing, what flows am I missing?" — and tighten the policies as the model sharpens: static assertions ("we never call exec"), then the invariants of each module (its preconditions, its post-conditions), then the properties preserved across module interactions. As you do this, every mismatch between model and code means one of exactly three things: the model is wrong and needs refining; the model is at the wrong level of abstraction for what you are checking, so you need a different model; or there is a genuine bug in the code — and the modeling just found it. Expect to find many. Shifting a legacy codebase from "the tests pass" to "these properties hold across all behaviors" surfaces defects by design. Some you will fix; some you will decide are working-as-intended and fix the model; some you will scope out by narrowing the model's assumed inputs. All three are legitimate moves.
4.1.4 Approach two: bottom-up, inducing the model from code
The second approach starts from the code and flies upward. Ask the agent to induce the model: "look at the codebase and help me develop the model inductively." Where the first approach starts from your coarse whiteboard understanding and refines down toward the code, this one starts on the ground and rises. Think of standing on a street and flying a drone up: the individual buildings blur, the street grid appears, then the highways, then the cities. As you climb, the levels of abstraction reveal themselves — and whether you came top-down or bottom-up, you are trying to meet in the middle, at the level that lets you enforce the properties you care about.
Which level that is depends entirely on the assurance you need, and the trade runs in proportion: the finer the model, the stronger the guarantee it can carry — and yet the more it costs to write and to run. For low assurance, just run the tests — the coarsest model is "this input, that output, checked dynamically." For the strongest guarantees, you need elaborate analyses over the control-flow graph and every function's inputs and outputs — expensive, because the checker must search every acre of the whole county. So begin by writing down what correct even means, because correctness is not one thing: it means one thing for security, another for semantics, another for privacy, and the definition tells you the abstraction level you need. Memory safety demands a fine-grained model that sees every access — and you will pay for it. A semantic property is often far cheaper.
Think of an audit trail on a spreadsheet: "every modification is stamped." You do not model every function to enforce that. You just require that every code path which modifies the ledger calls the stamp routine on the way out — a property about the order of two calls in the control-flow graph, not about what either call does. That is a high level of abstraction and therefore cheap to enforce. The companion catalogue ships this exact pattern as a product sensor: a lint that checks every format-mutating routine is wired to its provenance stamp, so an unstamped mutation cannot ship. The catalogue names this mechanism the provenance-stamp wiring lint. Proving the stamp routine itself is correct would need a much finer model — but if a human review or a test covers that, the coarse model is enough. Choosing the fitness function, then the abstraction that enforces it cheaply, is the new job, and it varies by codebase. It is yours to think through.
4.1.5 Approach three: lint, cover, then induce
Both approaches assume you can make sense of the code — and often you cannot, because it is a mess. Models read code the way humans do: they like comments that give rationale, names that reflect purpose, and shallow nesting, because deep nesting has to be held in working memory, and an agent pays for that context just as a human pays in confusion. So before you extract models from spaghetti, improve the code — and there is a loop for that, the same loop from Loops and Models. Define the inputs, the judgment, the output, and the metric. The metric here is a smell detector.
A smell is code that works but will break the moment it is touched, because no one can reason over it. You measure smells with cheap static analyses — lints (the dryer-lint kind, the stuff you pull out to make things tidy) — that parse the syntax tree and count the tells: a high density of bare ints and strings, deeply nested conditionals. Some rules are universal. No codebase should have a 2,000-line function, a module imported by every other file, a switch with 400 string cases, structs nested three anonymous levels deep, or functions returning unnamed many-element tuples. So run the lints, and have the agent fix them in a behavior-preserving way — which means you need tests. If you have none, start there: have the agent write tests for line or branch coverage, module by module, because coverage is what lets you refactor safely, using the tests and the lints together as your success metric.
Squash, zero, promote
Figure 4.1-2 draws how a lint is worn into a legacy tree without breaking any in-flight agent — the order is what keeps it safe.
Then comes the move that makes it stick: as each lint reaches zero, make it blocking. Now the success metric is "all tests pass and that lint still passes" — no more unnamed tuples, no more imports buried mid-function, ever again. This is the Alignment Thesis worn into a legacy tree one lint at a time: each promotion is a governance mechanism that bounds what any later agent can reintroduce. Order matters, and the companion repository holds to it deliberately — flipping a lint to blocking while findings remain would break every agent's commit at once, so the figure's sequence is the safe one: audit-only, drain to zero, then promote. The smells that leave never return. When you are done, you have cleaned-up spaghetti with enough shape that model induction has something to grip: classes and enums and named structs give the induction real structure to reason over, the structure that spaghetti hid. A companion mechanism closes the loop from the other side: it projects your test coverage onto the model's nodes, so an untested invariant shows up as a visible gap rather than hiding inside a line-coverage percentage. The catalogue names it coverage-to-model mapping.
The metric that points at what you missed
Induction gives you a first model, but it never tells you where it stops. You built the map of the concurrency-critical spine and the durable-state contracts; the rest of the code sits below the horizon, and nothing in the model announces its own edges. You need an instrument that points at the blank spots. The Missing Model Metric is that instrument, and it is the exact dual of coverage.
Coverage, run backwards
Coverage walks one direction: model to code. For each thing you modelled, find its code and ask whether a test touches it — that finds the modelled but untested. The Missing Model Metric walks the other way, code to model. For each test, reach the production code it exercises and ask whether any of that code traces up to a model node — an invariant, a state machine, a service edge. A test whose code reaches no model node is an orphan. Cluster the orphans by the unmodelled code they share, and you hold a candidate list of the models you have not written yet.
The duality is the point, and it is what makes the metric a modeling instrument rather than a testing one. You move a coverage number by adding a test. You move this one by adding a model. An orphan is not a hole in your test suite; it is a hole in your map. So the metric seeds the modeling backlog directly: it reads out where the induced model still owes the system a view.
Data from the case study
A pilot makes the shape concrete. Run it over the most heavily-modelled slice you have — for this system, the dispatch and orchestration subsystem, across 144 tests — and you would expect near-zero orphans, because if any subsystem traces cleanly this one should. It reported 56%. More than half the tests in the best-modelled corner exercise code that traces to no invariant, state machine, or service edge. A second pass sharpened that from a scolding into a map. Every orphan reached code the model did know — anchored at the coarse structural level, where a component owns a directory — but that carried no fine behavioral model. The structural map was complete; the behavioral map covered the concurrency-critical spine and stopped. The metric had found a granularity gap, and it named exactly where to close it: three genuine candidate models — the durable-state data layer, the subprocess-and-cost-rollup pipeline, the chunk merge-and-validation gate.
What earns the instrument its trust is that it discriminates. Two of the clusters it surfaced were not missing models. One was code below the model's grain — typed-id aliases, config loaders, enums — code that carries no behavior of its own to model. The other was a missing anchor: code that belongs to a node the model already has, just lacking the link back to it. The metric separates the three: missing model, missing anchor, and legitimately below the grain. Without that discrimination it would flag every unmodelled line and drown you. With it, the orphan clusters read as a work queue you can act on.
I ran the metric on my own system, as the last rung of the road in The Road to MAGE. There it drove a loop that closed the first several clusters and, in the process, caught real shipping bugs the passing tests had missed. That is the inward turn of this whole chapter: the brownfield recipe applied by the codebase to itself.
You will need all three approaches in combination. There is no magic in them, only hard work and judgment. But applied together, and scaled to the quality of your starting point, they land you at the destination: the right models, encoding the right views, with the right correctness criteria, traceable to where each is enforced — statically, dynamically, or by a model check.
I owe you honesty about the evidence here. This is a solo field report, not a controlled study. I did not run the migration twice, once with governance and once without, and measure the delta — I had one codebase and one pass through it, so I cannot hand you a clean before-and-after number. Take what follows as lived experience, weighed as such.
How much governance is enough
Which raises the real question: how much governance is enough? There is no formula. There is a judgment, and it turns on two factors multiplied together — how much a failure costs you, times how often it happens. Cost is more than product risk, of course. An agent that trips over a failure burns tokens working around it, and tokens are real dollars, so a cheap-looking failure that every agent stumbles into ten times a day is expensive in a way the incident log never shows. Multiply the two. The costlier and the more frequent the failure, the harder the mechanism it earns — a blocking lint, a gate, a structured model that makes the mistake impossible. Low times low you leave to judgment; a mechanism there costs more attention to maintain than the failure ever costs you. The middle band is where taste lives, and where you will spend it. This is also when to stop: a mechanism guarding a failure that is both rare and cheap is the first brick in the teetering tower The Skills warns against.
Draw the two factors as axes and the judgment gets a shape. Table 4.1-1 crosses how much a failure costs against how often it happens, and each cell names the proportionate move — the smallest change that still closes the class. Two of the cells carry the part people skip. In the costly column the first move is not a control but the architecture: make the mistake impossible before you reach for something that catches it, because a caught mistake still happened. And the costly-and-frequent corner earns both — the structured model that forecloses the mistake and a gate that holds the line while the model is still spreading. The reflex to reach for a control on every failure is the expensive habit; float the larger scheme, and let the cost of the failure justify building it.
| Cost × frequency | Rare failure | Frequent failure |
|---|---|---|
| Cheap | Leave it to judgment. A mechanism here is the first brick in the teetering tower. | A cheap lint or script. It trips agents often, and the tokens they burn working around it are real cost. |
| Costly | A runbook with a named escape; a blocking gate once the failure's shape is decidable. | Architecture first: a structured model that makes the mistake impossible. Then a gate on top. This corner earns both. |
The litmus for when to stop falls out of the same two axes:
Before you build a control, price its own upkeep. Reach for the smallest change that closes the whole class, not the one failure in front of you, and float the larger scheme until its cost is justified. If the control would guard a failure that is both rare and cheap, its upkeep will cost more attention than the failure ever will — so leave that one to judgment.
4.1.6 How I know this
I know this because I lived it. By April I had 300,000 lines of code that were very janky — a Rube Goldberg machine that worked but was a Rube Goldberg machine, because I had started by needing an MVP to see if the thing was possible at all. I had imposed real control only on the parts I knew were sensitive, where I pushed hard for a strong architecture. The rest — the web layer, the front end — I hoped the agent would just get right. It did not, and those parts turned crufty. I told it to use typed Python; it did, and it was typed, in a sense. It specifically used the type string. That was the type it used. So it was stringly-typed: no actual safety the compiler could give you, just the word "type" satisfied to the letter and voided in spirit. I told it to retrofit real types; it turned on the type checker and made everything strings and ints, without building the type abstractions I actually wanted.
So I spent a month retrofitting these ideas onto the code base, and when I was done I had 200,000 lines of code that were not janky. The system got smaller as it got better — structure is compression, and the primitive sprawl was the bloat. Those round numbers, 300,000 janky lines in and 200,000 clean out in a month, are my April estimate of the episode, not a reconstructed count. What lifts them past memory is that the project's measured churn corroborates their shape. For the data, see The Timeline and the Work → (preliminary) The add-and-delete peaked exactly while I did this work, then the deletions collapsed and the later windows went net-additive — the signature of a one-time replacement, not steady thrashing. And the churn was productive: what I deleted was the stringly-typed sprawl, what replaced it was modeled, typed structure, which is why the code shrank as it improved. So read the numbers as the estimate that churn backs, not a hand-counted ledger — the add and delete totals sweep in generated and vendored files, so they signal motion, not hand-authored lines. It is real because I did it. The same thing had happened porting JavaScript to TypeScript that same week — a hundred thousand lines of "typed" Python and a hundred and fifty thousand of TypeScript, each ported the cheapest way possible, because the agent had no model and did not know the names of things. It is probably better that it did not guess.
So I retrofitted the models onto the mess — moving the code toward a clean model-view-controller architecture, moving the web stack to reactive, loop-based structures friendly to auto-scaling. And I did it exactly the way this chapter describes. I turned on every lint from Ruff, Pylint, and ESLint — hundreds of commodity smell checks — and for several weeks the agents squashed them, making each one blocking as it went to zero so it could never come back. I knew the coarse architecture I wanted and said "pursue this model," but I had to get there iteratively: find the dense primitive regions, induce structure there, and inch toward the target.
A dense primitive region is a stretch of code doing many low-level operations by hand — raw library calls, strings and ints passed around untyped, tree-walking spelled out inline — with no abstraction that owns them. I found these the crude way, by reading for the clots: where the primitive calls piled up thickest, the missing model was loudest. Inducing structure means extracting a structured model that owns those primitives — one class the raw calls now route through — and rewriting the region to go through it. You do this region by region, not all at once, because each induced model gives the next region something to lean on, and the target architecture assembles out of the pieces. That is what the squash-to-zero does at the fine grain, too: each lint I drove to zero was one primitive smell given a name and a home, and the model-view-controller decomposition is just the same move at the coarse grain. After a while, it worked. Now I have the models, the architecture, the code that implements it, and enforcement holding all three together every time the code changes.
In MAGE terms, that month was governance conversion at brownfield scale: the recurring failures and the same rediscoveries, over and over, became the models, links, lints, and gates that now shape every later task.‡‡ Governance conversion turns a recurring failure class into a mechanism the environment applies for you, so a later agent meets the check instead of rediscovering the failure.