The same bug, twice
In late 2024 a payments team I was on shipped a fix for a single line of code. A retry loop had been catching too broad an exception class and silently swallowing rate-limit errors from an upstream gateway. The fix was three characters. The PR was approved in seven minutes.
Eleven months later, a different engineer, onboarded after the original author had moved teams, refactored the retry loop. The line came back. Not maliciously; the new shape "looked cleaner." The fix had not been written down anywhere except in the diff history and the bug ticket, both of which the new engineer reasonably did not read before refactoring code that "wasn't broken."
The customer-facing symptom returned. The on-call woke at 4 a.m. The team filed the same bug ticket. The fix took eleven months to die and seventeen seconds to re-introduce.
This is the story this chapter is about. Bugs are clauses, operator-facing facts about what the product does, that get discovered the hard way. If the only place the clause lives is in a fix, the clause is one engineer's memory away from being lost. If the clause lives in the blueprint, the next regeneration emits the test that catches it again, and again, and again.
This chapter is about how to make every bug-fix grow the blueprint. Not as documentation discipline; as the engine that turns the blueprint into the operator's complete intent over time.
"Program testing can be used to show the presence of bugs, but never to show their absence." Edsger Dijkstra, Notes on Structured Programming, 1970 "A bug is not a defect. A bug is a clause the operator forgot to state. Each one shows the absence of a test that should always have existed." this paper
Has a bug come back to your team three times? That is not memory failure. It is the clause living in someone's head instead of in the blueprint. This chapter is about that gap and how to close it.
3.1 Bugs are clauses
Start with the reframing. The bug in the opening case, a too-broad exception class, was not a defect of the code. The code did exactly what it said. The defect was that what the operator wanted, "rate-limit errors must surface as retryable, not be silently swallowed", was never stated executably. The team knew it. The original author knew it. The retry loop, on its own, didn't know it.
A bug, then, is a clause the operator forgot to state. Or: a clause that lived only in the team's heads, until enough turnover happened that nobody remembered it any more, and the code drifted away from it.
This reframing changes what a bug-fix is for. In Builder culture, the Builder's world, sketched in the previous chapter, a bug-fix is a code change that makes the symptom go away. Patch the line, run the tests, deploy, close the ticket. The bug ticket dies; the fix dies with the next refactor; the cycle repeats.
In Architect culture, the Architect's world, a bug-fix is a promotion. The clause is promoted from "implicit operator expectation" to "explicit executable assertion." The code change is a side-effect of writing the test that asserts the clause. The bug ticket dies; the clause persists; the next regeneration emits the test that catches the clause again.
The promotion is one-way. Once a clause is in the blueprint, removing it requires a deliberate decision. The blueprint accretes. It only grows.
A bug is a clause the operator forgot to state.
The arrow from F back to A is the load-bearing one. Once the test is in the blueprint, the bug cannot recur in any future regeneration. Because the test gates the deploy, and the test was emitted from the blueprint, and the blueprint has the clause. The cycle is self-extinguishing. Which is exactly what bug-fix loops in Builder culture are not.
A structural-debt note. The eleven-months-to-re-introduce story is not a memory failure. It is the predictable outcome of a system where the clause has nowhere to live. Every team that fixes the same bug twice is paying a tax for not having a blueprint. The patch-and-forget loop isn't laziness. It's gravity. The fix is structural: give the clause a home, or watch it leak again on the next refactor.
3.2 The blueprint is the test corpus
In Chapter 1 I called the deliverable a test suite. In Chapter 2 I sharpened it to deliverable tests. Here, in Chapter 3, the right word is blueprint. And it is time to say precisely what the blueprint is.
The blueprint is the contract-test corpus: every test that asserts a clause about the product's operator-facing behaviour, expressed as a fixture with concrete input data and expected output. Chapter 2's Zod-validated POST /api/v1/jobs test is a clause. The Playwright "no-stuck-loaders" journey test is a clause. The lifecycle test that drives a Job through pending → claimed → running → succeeded is a clause. The corpus of those tests is the blueprint.
There is also a spec.json. But spec.json is not a parallel description of the product. It is a small machine-readable index over the test corpus. It carries metadata about each clause, not a re-implementation of the clause in JSON. The shape, concretely:
``json { "uiPages": { "/": { "clauses": [{ "test": "tests/playwright/dashboard-recent.spec.ts:25", "summary": "logged-in visitor sees counters + jobs in 10s", "surface": "frontend", "source": "bug:warp#964", "added": "2026-05-02" }] } }, "apiContracts": { "POST /api/auth/login": { "clauses": [{ "test": "tests/contracts/auth-login.spec.ts:48", "summary": "wrong password and unknown user return same 401", "surface": "backend", "source": "security-review", "added": "2026-04-12" }] } } } ``
Notice what spec.json does NOT carry: the test body, the input fixtures, the assertions, the expected output. Those live in the test file. The file the index points at. Trying to mirror them in spec.json would be writing the same artefact twice in different syntaxes.
Each entry in the index carries provenance (source), date (added), surface (where the regression test landed), and a one-line summary for human eyes scanning a hundred clauses without reading test code. That is all the metadata layer needs.
The factory and the operator use the index differently. The factory reads spec.json to know which tests must pass on every regeneration; it walks the index, finds the tests, runs them, gates the deploy on their result. The operator queries spec.json to answer questions like "which clauses came from bug warp#964?" or "which clauses are stale because the auth flow changed last quarter?". Questions the test corpus alone cannot answer in seconds. Two consumers, one index.
The relationship to hold in your head: the test file is the clause. spec.json is the table of contents. Three layers, index, contract, satisfier, not four. The blueprint is the contracts. The implementation regenerates against them.
3.2b Why the spec stays thin
A reasonable engineer reading this far will ask: why not make the spec richer? Add the prose. Add the rationale. Add the operator's full intent in given/when/then form. Surely a more detailed spec is a better spec?
If your spec reads like the code, you've lost. The blueprint is examples, not abstractions.
It is the right instinct and the wrong move. Here is the failure mode I have hit, repeatedly, in the years before this book.
Blueprint inflation. I tried, on three separate products, to make spec.json a maximally-detailed description of every behaviour. By the time the description was complete, it was a re-implementation of the code in JSON. Every conditional in the implementation had a corresponding clause in the spec. Every error path had a clause. Every edge case had a clause. The spec was no longer smaller than the code; it was the same artefact in a different syntax. I had not gained any speed. I had built two systems that needed to stay in sync. And the second one (the prose spec) drifted from the first one (the test corpus) within weeks, because nobody noticed when they fell out of step.
The value of the blueprint comes from what it does NOT contain. It does not describe how the code is structured. It does not describe which framework, which language, which storage layer, which middleware chain. It does not describe internal data shapes the operator never sees. It does not describe error paths the operator does not care about. The blueprint is what the operator can observe, expressed as concrete examples a test can verify, and that is all.
This is the lesson the contract-test corpus encodes naturally and the prose spec inflates against. A contract test like "POST /api/v1/jobs with {kind: 'index'} returns 201 with a body matching the Job schema" is concise, executable, operator-facing, and complete. It pins the behaviour that matters. It does not also tell you which Postgres column the Job is stored in, because the operator does not care, and neither should the blueprint.
So: the contract test, with concrete input data and expected output, is the practical unit of the blueprint. The spec.json is an index over those units. The prose layer above the index is the brief, one paragraph, the operator's framing, and that is where prose belongs. Not in the contract layer. Not in the test layer.
This reframes a conventional question. Every BDD or Pact tutorial since 2014 has gestured at this: "the test is the contract." What is new is the reason. In a Builder-culture world, contract testing was a discipline some teams adopted; in an Architect-culture world with AI authoring, the contract test is what survives both your codebase outgrowing your head and a better model invalidating last year's build. The operator's intent, captured as concrete I/O examples, is portable across any implementation that can be written. The same intent captured as prose is a wish.
The deeper point. A purely declarative spec, "for all valid Job records, the round-trip property holds", is non-constructive. It claims a property without telling you how to verify it. A test with a concrete generator and concrete assertion is the verification. In a regeneration world, the AI does not need the abstract description; it needs examples it can match against and invariants it can test against. The contract test gives both. The prose layer gives neither.
An architecture note. The reframe here is the boundary. The contract test sits at the operator-observable boundary; everything above is the brief, everything below is implementation. When operators try to push the spec layer into describing implementation details, they are violating the boundary, and the cost compounds. Drifted prose, two-systems-of-record, the spec ageing into folklore. Keep the spec at the observable boundary. The factory will fill in everything below.
3.3 The growth contract
The mechanism that grows the blueprint is a small change to the bug-fix workflow. In Builder culture, fixing a bug means: (1) find it, (2) write a fix, (3) ship. The growth contract replaces step 2 and adds one step:
1. Find the bug. 2. Write the failing contract test. Phrase it in given/when/then form, with concrete input data and expected output. Run it against the live system. It fails. The test IS the clause. There is no separate prose to write. 3. Write the fix. The test now passes. 4. Ship. 5. Append the test's metadata to the index. On merge, gyrum-complete-pr adds an entry to spec.json. Pointer to the test file, surface, source ticket, date.
Step 2 is the engineering work, and it is also the spec authoring (no separate prose step). Step 3 is automatic once step 2 is done. Step 5 is automatable. Gyrum-complete-pr does it on merge for PRs carrying surface: and test: fields.
A TDD note. Step 2 is the load-bearing one and it's the step everyone wants to skip. Write the failing test first, with real input and expected output. If you write the fix first, "the test passes" is meaningless. You have no evidence the test was ever capable of failing. I have seen too many regression tests that were written after the fix and pass on green code because they assert on the wrong thing. Red → Green → Refactor isn't a preference; it is the only way the test earns its place in the blueprint.
The growth contract turns every bug-fix into a blueprint-grower. Over a year, that's hundreds of clauses. Over five years, thousands. The blueprint becomes a complete operator-intent document. Not because anyone planned it, but because every bug-fix grew it by one.
The clause is now permanent. Tomorrow's agent (human or AI), regenerating the codebase from spec, will emit the same test. The bug cannot recur unless the operator deliberately removes the clause.
This is what makes the growth contract qualitatively different from "just write a regression test, like every team has done since 2003." A regression test sits in the test suite as a sibling of the implementation; if the implementation is regenerated, the test may or may not survive (Builder culture's leakage problem). A blueprint clause is upstream of the implementation. The implementation regenerates from it. The clause is the contract; the implementation is the satisfier.
Replay the opening bug with the growth contract running, and watch the eleven-month story collapse. The retry loop swallows the rate-limit error. The engineer who picks up the ticket does not patch the line and close it. They write the failing contract test first: given an upstream gateway returns a rate-limit status, when the retry loop runs, the error surfaces as retryable and is never swallowed. They run it against the live system and it fails, because the loop is catching too broad a class. That red test is the clause. Then the fix, the same three characters as the original, and the test goes green. On merge, gyrum-complete-pr appends one line to the index: a pointer to the test, its surface, the source ticket, the date. Now wind forward eleven months, to the refactor that "looked cleaner." A new engineer rewrites the loop and, exactly as before, never reads the old bug ticket. But this time the rewrite has to pass the blueprint's tests before it can deploy, and one of those tests is the clause the first engineer landed. The cleaner shape catches the broad exception class again, the rate-limit test goes red, the deploy is blocked, and the regression dies in CI at 2 p.m. on a Tuesday instead of in production at 4 a.m. eleven months later. Nobody remembered the fix. The blueprint remembered it for them, which is the whole point: the clause outlived the author, the ticket, and the refactor, because it had somewhere to live that a refactor cannot reach.
3.4 What the index carries (and what it doesn't)
Given that the contract test IS the clause, pinned in section 3.2b, what does spec.json carry, and why have it at all?
Three reasons. Each is about metadata over the test corpus, not parallel description.
Reason 1 - multiple test-shapes per clause. A single deliverable behaviour may need multiple tests at multiple layers. The "tenant A cannot see tenant B's jobs" clause needs a contract test (on the list endpoint), a journey test (the UI doesn't show them), and a multi-tenant boundary test. The index lets you answer "what tests assert this clause?" without grepping ten thousand lines of test code. One clause-id in the index points to N tests. A regeneration that produces only one of the three layers fails the index check. And that is exactly when you want it to fail.
Reason 2 - language portability across regenerations. A contract test written in Playwright/TypeScript runs only on a JS-shaped frontend. If the factory regenerates gy-queue as an HTMX server-rendered app next quarter, the Playwright test may not apply unchanged. The index lets the factory know what behaviour must be re-tested (the clause's one-line summary) and under which test-shape constraints (frontend → Playwright; backend → REST contract; mobile → native runner). The factory re-emits the test in the right shape; the index tracks that the behavioural intent is the same across re-emissions, even when the test code is regenerated.
Reason 3 - provenance and audit. Operators ask "which clauses came from bug warp#964?" and "which clauses came from the original brief?" and "which clauses are stale because the auth flow changed last quarter?" The index answers those questions in seconds; the test corpus alone cannot. Provenance is metadata; metadata belongs in the index.
Notice what the index does NOT carry. The test's body. The fixtures. The assertions. The expected outputs. Those live in the test file. Mirror them in spec.json and you have built the inflation failure mode of section 3.2b by hand. The index is pointers and metadata; the corpus is contracts; the implementation is the satisfier. Three layers, each load-bearing, none redundant.
The trade-off is a small amount of duplication: the one-line summary of a clause appears both in the index (as searchable metadata) and in the test file (as the test's name or given/when/then header). That duplication is mechanical and stays in sync naturally. The test name is the summary; the summary is the test name. The contracts and the index grow together; the prose layer above them is the brief, not a parallel spec.
The clause is the source. The tests are derived. If the operator decides "we no longer need to support the mobile target," they remove the mobile-test generator. If they decide "we no longer want to leak existence on login errors," they remove the clause; both the contract test and any journey test that asserted the clause go away. The blueprint shapes the test suite, not the other way round.
3.5 What accretion looks like over time
A thought experiment. Imagine three points in a product's life.
Day 0. A one-paragraph brief. The operator types: "Build a multi-tenant job queue REST service in Go + SvelteKit." The factory reads the brief. It generates a baseline spec.json containing about a dozen obvious clauses, among them: registration works, login works, jobs can be submitted, jobs can be listed. The factory builds the product. The product ships.
Day 30. The operator has used the product. Six bugs have been filed and fixed. Each fix appended one or more clauses to the blueprint. By day 30, spec.json has roughly twenty-six clauses beyond the original twelve, about thirty-eight in all. The blueprint has more than tripled. Each new clause covers a previously-unstated assumption: "the dashboard renders within 10s," "tenant boundaries are enforced on GET /jobs/:id," "wrong-password and unknown-user return identical errors," "the SSE stream resumes after a 30-second idle without dropping events."
Day 365. The operator has shipped weekly for a year. The blueprint has perhaps four hundred clauses. Most came from bugs. Operator-facing facts the implementation didn't satisfy until forced to. Some came from security reviews. Some from compliance audits. A handful from the operator suddenly noticing "ah, this should have always been true" and adding a clause without a triggering bug.
Most engineers reading this will find the curve aspirational. They will note that their own systems have not followed it; their bug-fixes have been Builder culture patches; their accreted blueprint is empty. The growth contract is what makes the curve achievable. Without it, the curve is whatever an unusually-disciplined engineer remembers to do; with it, the curve is the default behaviour of the system.
The operator at Day 365 owns something the operator at Day 0 did not have: a complete description of what the product does, in terms that survive any rewrite. If the team disbands and a new agent, human or AI, picks up the spec, they can rebuild the product faithfully. The clauses are the contract; the implementation is recovered from the contract.
This is why the blueprint is the asset. The codebase at Day 365 is the result of weekly factory runs against Day-365's blueprint. If the codebase were lost, accidentally, deliberately, or because the framework it was written in went out of fashion, the factory could rebuild it next week. As long as the blueprint survives, the product survives.
3.6 What can go wrong
Three failure modes are worth naming, because they will happen.
Failure mode 1 - clause inflation. The operator (or an over-zealous engineer) starts adding clauses for every conceivable property. By day 90, the blueprint has 500 clauses, half of which are not really operator-facing. They're implementation constraints disguised as deliverables. The test suite balloons. Builds slow. Engineers start ignoring failures of "non-essential" tests.
The mitigation is the four-property check from section 2.1. A clause that fails Property 1 (operator-facing) or Property 2 (implementation-agnostic) does not belong in the blueprint. It belongs in the unit-test scaffolding around the implementation. Builder culture territory. The blueprint review process should catch these on entry.
Failure mode 2 - clause contradiction. Two clauses, added at different times, contradict each other. Clause A (added 2026-03-15) says "login error is generic." Clause B (added 2026-09-01) says "login error names the missing field." Both pass review at the time they're added. The factory tries to satisfy both; both tests fail.
The mitigation is detection: a periodic blueprint-consistency pass that runs every clause's tests against an idealised satisfier and flags pairs that cannot both pass. The implementation is non-trivial; the principle is simple. A blueprint that contradicts itself is a blueprint that no factory can satisfy.
Failure mode 3 - clause ownership becomes unclear. Three years in, the blueprint has a thousand clauses. Half were added by people who have left the team. A new engineer wants to remove a clause that seems wrong. But cannot tell why it was added or whether it's still needed. They leave it. The blueprint becomes a museum.
The mitigation is the source: field. Every clause records who added it (or which bug ticket it came from). When the originating engineer is gone, the bug ticket is the next-best context. And warp tickets in the gyrum fleet are durable. A clause whose source: resolves to a closed ticket can be reviewed in context. A clause with no source: is debt and should be marked as such.
These failure modes are real. None of them are catastrophic. The blueprint pays for itself many times over even with all three failure modes occurring at modest rates. The mitigations are operational hygiene, not deep redesign.
3.7 Why "regression test" is the wrong name
This chapter has avoided the phrase regression test, which is the conventional term for "a test you write because of a bug." That phrase is wrong for the Architect culture model, and the wrongness matters.
A regression test is, by its name, defensive. It sits in the suite to catch the bug if it comes back. Its purpose is negative: prevent a known bad outcome. The cultural connotations are right for Builder culture. Tests as scaffolding, this one specifically here to keep an old wound from re-opening.
A blueprint clause is, by its name, generative. It sits in the spec to describe the product. Its purpose is positive: state what the product does. The cultural connotations are right for Architect culture. Clauses as the operator's intent, this one expressing what the product is supposed to be.
The same code can be either. A Playwright spec written after a bug-fix is a regression test if it lives only in tests/ and gets deleted in cleanups. The same Playwright spec is a blueprint clause if it has a corresponding entry in spec.json with provenance, if it is regenerated on every factory build, and if the operator can read its prose form to audit.
Names matter. Regression test invites the test to be deleted later. Blueprint clause invites it to be preserved. The naming is part of the engineering practice.
Regression test invites the test to be deleted later. Blueprint clause invites it to be preserved.
3.8 What this chapter is for
If Chapter 1 made the framing precise (two cultures) and Chapter 2 made the artefacts concrete (four shapes), Chapter 3 makes the engine visible: the bug-fix workflow as the mechanism by which the blueprint grows.
The chapter is short on numbers and long on framing because the engine is a workflow change, not a calculation. The change is: every bug-fix promotes a clause; every clause stays. After a year, the blueprint is complete. Not because anyone planned it but because every bug pushed it forward.
The next chapter, Seven Corners of Reliability, argues that the test stack we have today (unit + contract + journey) is missing four corners (property + lifecycle + multi-tenant + cross-system). Those are the corners where the most expensive operator-facing bugs hide. Once the growth contract from this chapter is running, the kinds of clauses the blueprint accretes will gradually include all seven layers, but only if the team knows to look for them.
Try it: pick the last bug you fixed. Write the deliverable clause it surfaced. Given/when/then, in operator-facing terms. Now ask: is this clause anywhere except in the diff and the ticket? If not, that fix is one refactor away from being lost.
If the blueprint accretes one clause per bug, what kinds of bugs does the typical test stack still miss? More than you'd guess.