The Next Test
Using AI to drive Transformation Priority Premise–based TDD
Test-Driven Development gives you a loop: write a failing test, make it pass, refactor, repeat. What it conspicuously does not give you is the one decision that actually shapes your design — which test do you write next? Pick the wrong one and you can paint an algorithm into a corner three steps before you notice. Pick the right ones, in the right order, and the design seems to assemble itself.
Robert Martin's Transformation Priority Premise (TPP) is an answer to that question, and so is James Grenning's ZOMBIES mnemonic. Both say the same thing: of the tests you could write next, prefer the one whose passing requires the simplest change to the code. Rank the transformations — from "return a constant" up through "introduce a conditional," "introduce a loop," "generalize a variable" — and always reach for the lowest rung still available. As Martin puts it: as the tests get more specific, the code gets more generic.
I wanted to know two things. First, does that heuristic actually hold up when you follow it honestly? And second — the part I really cared about — what happens when you hand the mechanics to an AI agent and let it drive? I'd written a small skill that teaches an agent the TPP/ZOMBIES ranking, and I wanted to validate it on real work.
So I spent a few sessions pairing with an AI agent on coding katas in modern C++26, on rails I trust: CMake and Catch2 for the build and tests, lizard and jscpd watching complexity and duplication, and — crucially — Mull for mutation testing, because a green bar proves nothing on its own. Four katas came out of it: Bowling, Prime Factors, a Reverse Polish calculator, and Word Wrap. Here's what I learned.
You cannot fake emergence
The very first run — Bowling — went suspiciously well: a clean sequence of slices, each with a tidy note naming the TPP transformation it drove, green all the way, 100% mutation. And it was subtly wrong. The agent had authored every test up front, in one batch, in a fixed order, then implemented against them one by one. The per-step rhythm looked emergent; it wasn't. The test list and its ordering — the very thing TPP is about — had been decided in advance, and the per-step "transformation" notes were post-hoc justification of a predetermined plan. A famous kata made that easy to hide: of course the order looked principled; the agent already knew the answer.
So the first real lesson is about the human, not the AI. TPP is a discipline of discovery, and discovery can't be precomputed. You keep a backlog of candidate behaviors, but you write each test just in time, watch it fail, make it pass, and only then decide what the code is asking for next. An agent will happily generate the whole suite, because generating is cheap for it. Keeping it honest — one test at a time, selected live — was my job.
When it works, the design falls out
We did the next kata, Prime Factors, the right way: one test, then a decision, then the next test.
Prime Factors is the canonical TPP demonstration, and watching it emerge under that discipline is genuinely satisfying. primeFactors(1) returns an empty list — a constant. primeFactors(2) returns [2] — read the input. Then primeFactors(3) returns [3] — and it already passes. That confirming test, the one that drives no production change, is the whole point: it tells you the primes are handled and you now need a composite to make progress. primeFactors(4) forces the first real loop. And primeFactors(9) forces the divisor 2 to become a variable — at which point the general algorithm is simply there:
for (int divisor = 2; n > 1; divisor++)
while (n % divisor == 0) { factors.push_back(divisor); n /= divisor; }Nobody designed that. It accreted, one smallest-possible transformation at a time. And that confirming primeFactors(3) — the test that passed with no new code — turned out to be a recurring payoff: Bowling had the same moment when the perfect game (a 300) needed zero production change, because the design had already generalized past the boundary. That's ZOMBIES' "Boundary" case earning its keep, and it only shows up if you're genuinely discovering rather than scripting.
There's a sharp corollary, too. On Bowling, one step had been too big — going from a flat sum to full frame-by-frame scoring in a single leap. TPP has a name for that smell ("a test forces a huge implementation leap → a smaller step is missing"), and the fix is Kent Beck's old advice: make the change easy, then make the easy change. In the RPN calculator we applied it deliberately — a behavior-preserving refactor to introduce a tokenizer and stack first, so that adding the actual operator was a one-line change. The agent proposed it by name. The heuristic had become something we could reason with, not just recite.
It scales further than a toy
The Reverse Polish calculator is where I stopped worrying about whether this was just kata theater. Driven the same way — test by test — it grew from "" → 0 into something with real design content: a calculator generic over its numeric type via a C++20 concept, returning std::expected for its error paths, and computing by default in a custom fixed-point decimal type I asked for mid-stream. None of that was planned at the outset. Each piece arrived because a test demanded it, and TPP kept each arrival small.
What I found persuasive is that the human decisions — "make division return a fixed-point type," "could this be a concept instead?", "require equality_comparable" — slotted into the emergent flow without breaking it. The agent supplied the mechanics and the next-test discipline; I supplied the design intent. That division of labor felt right.
When the order is the whole game
If any kata earns this piece's title, it's Word Wrap. Robert Martin calls it "the hardest kata I know" — not because the code is hard (the finished function is a dozen lines) but because the order you choose the tests decides whether the design falls out cleanly or fights you. It is the thesis of this whole piece compressed into one exercise.
Driven test by test, a greedy recursive wrap emerged in three steps: empty text returns empty; text within the width returns unchanged; text past the width breaks at the last space and recurses on the rest. A few lines, no design up front.
Then came the pivotal choice — exactly the "what next?" the heuristic exists to answer. Two candidates sat in the backlog. One was another multi-word line: safe, satisfying, almost certainly green on the first try. The other was a single word longer than the width, with no space to break on. I took the second.
The RED wasn't an assertion failure. It was a core dump. With no space to find, the recursion never shrank its input and ran until the stack gave out. The hard-break case had been a latent non-termination bug all along — and the order is what exposed it. Had I taken the comfortable multi-word test next, it would have passed, the bar would have stayed green, and the crash would have waited for some unlucky input in production. "Which test next" isn't a stylistic preference; it's the difference between finding a defect now and shipping it. The agent, to its credit, had ranked the unhandled shape boundary above the redundant happy path example — but it still took a human choosing to trust that ranking over the safer-feeling test to make the bug fall out on cycle four instead of in the field.
The same kata produced the cleanest example of the other half of the job. Later I had the agent add whitespace normalization — collapse runs of spaces, trim the ends. It worked; every test green. It also roughly doubled the size of the function and dragged two new mutants out of the woodwork. So I reversed it. Wrapping and normalizing are two jobs; a caller who wants clean whitespace can normalize before calling wrap. The lesson is one no test-selection heuristic will ever hand you: an edge case isn't worth handling just because you can. TPP tells you the next test for the behavior you've chosen to build — it has nothing to say about whether that behavior belongs in the function at all. That call is yours.
Mutation testing is the truth-teller — until it isn't
Throughout, mutation testing was the adult in the room. Catch2 turning green tells you your tests pass; Mull tells you whether they'd notice if the code were wrong. Several times it caught exactly the gaps TPP style example tests miss — a sign-of-zero rendering, a multi-digit parse, a fractional digit never exercised. Real gaps, killed with targeted tests.
But the RPN calculator also surfaced the most interesting limit of the whole exercise. Making the calculator generic turned it into a header-only template, and the fixed-point type was constexpr. Mull started reporting survivors I knew the tests caught — and indeed, hand-applying the same mutations at the source level failed the suite every time. The tests were adequate. Mull simply cannot reliably mutate inlined constexpr/template code: it mutates one instance while the executed, inlined instance stays untouched. The 87% it reported was really "100% of the surviving mutants, minus the ones the tool structurally can't reach." That's not a hole in the tests; it's a genuine, demonstrable trade-off between modern generic C++ and current mutation tooling — and I'd never have pinned it down without an agent willing to mechanically hand-mutate eleven survivors and classify each.
Word Wrap let me act on that finding rather than just document it. Knowing a header-only function would be invisible to Mull, I had the agent build it as an ordinary compiled library instead. Same harness, one structural choice — and Mull mutated the real object code while the tests killed every mutant. A genuine 100%, no asterisk. The RPN limitation wasn't a dead end; it was a constraint I could now design around. (Even there, a small detour was instructive: when normalization briefly lived in the function, Mull flagged survivors that hand-mutation proved the tests did catch — a single static helper the compiler had inlined, the same blind spot in miniature. Reverting the detour made it moot, but it was a tidy reminder that the tool sees object code, not intentions.)
Who's actually driving
Here's my honest read. The AI is extraordinary at the mechanics: writing the next test, running the red bar, implementing the minimum, refactoring, keeping the build and the lints and the formatting green, and — given the skill — proposing the next test by transformation rank with a real rationale. It compresses the tedious parts of TDD to almost nothing.
But it does not, on its own, want the discipline. Left alone it batch-authored a suite and called it emergent. The value of TPP as a skill is that it gives the agent a principled answer to "what next?" so its speed is pointed in a good direction. The value of the human is everything the heuristic can't decide: noticing when emergence is being faked, choosing the design intent, and knowing when an 87% mutation score is actually a 100% with an asterisk.
TPP, Martin was always careful to say, is a premise, not a theorem — there's no proof you get better designs by always taking the higher-ranked transformation. After four katas I believe it as a steering heuristic, not a guarantee. What's new is that an AI agent makes the discipline cheap enough to follow faithfully on every single step — if you keep it honest about what "the next test" really means. Word Wrap put the finest point on it: the agent can rank the candidates, but choosing the test that hurts — the one that drags a latent crash into the light on cycle four — is still a decision a person has to be willing to make.
Postscript: I went back and did Bowling honestly
The opening of this piece nagged at me enough that I reran Bowling — properly this time, one test at a time, no list authored up front — to find out what honest emergence would actually have changed.
The answer is: not much, and that turns out to be the interesting part. Bowling's algorithm is small enough that there's essentially one shape to find, and both runs converged on the same frame-by-frame loop. Doing it "right" did not produce better code.
What it changed was the journey. The spare — the step the first run took in one oversized leap — split cleanly into two: a behavior-preserving prep-refactor (turn the flat sum into a frame walk), then a one-line bonus. The heuristic saw the leap coming and asked for the smaller step; honest emergence took it. (The strike's RED, as a bonus, arrived as an out-of-bounds crash rather than a wrong number — emergence has a way of making the gaps loud.)
And here's the part that actually reassured me. Driven blind, the second run independently rediscovered the same two landmarks: the perfect game that needs no new code, and the same lurking mutation survivor — closed by that same perfect game. The first run's emergence was front-loaded, but it wasn't fiction; the design really is the one the tests pull out of you. For a kata this small, that — a safer sequence of steps, plus the confidence that the shape is real — is the whole prize. Don't oversell what redoing it bought: honest TDD didn't rescue a bad design here, because there wasn't one to rescue. It bought a gentler path and a clear conscience. On a problem with genuine room to go wrong, I'd expect the gap to be wider — but that's a claim for a harder kata, not this one.
The code, the full emergent traces, and the mutation analyses live in the kata repository (github.com/dale-stewart/kata); this piece is drawn from that work.
