A confound in measuring AI code degradation, and why we deleted every test to fix it.
We have been running a long experiment. Take one feature backlog. It is sixty small, ordered changes to a PetClinic REST service. Have an AI agent implement them one checkpoint at a time. Do it into two different codebases.
One arm is built the conventional Spring way, with controllers and services. The other is built with OfficeFloor and its composed-function architecture. The question is not whether the AI can do it. Both arms stay green almost all the way. The question is how the code decays.
The early runs told a clean story. These were the ones we pushed to GitHub. Spring eroded noticeably worse than OfficeFloor.
It was also partly an artifact of our own measurement setup. At least we now believe so. This post is about the confound we found.
The symptom
Structural erosion here is borrowed from SlopCodeBench. It is the share of a codebase's complexity "mass" that lives in functions above a cyclomatic-complexity threshold. Low is good. Complexity is spread thinly across small functions. High is bad. Complexity is piled into a few fat methods.
In the pushed runs, the final-state numbers looked like this.
| Arm (pushed run) | Final erosion | Create-endpoint handler CC | True regressions |
|---|---|---|---|
| Spring | 16.9 % | addOwner grew to CC 27 |
4 |
| OfficeFloor | 10.5 % | entry handler flat at CC 1 | 8 |
Two things in that table sit oddly together. Spring eroded more. Its addOwner method ballooned into a 27-branch monster. Yet Spring also regressed less. It had 4 genuine regressions against OfficeFloor's 8. An arm that is quietly accumulating complexity is usually the arm that is quietly breaking things. It is not usually the one breaking fewer.
That mismatch was the thread worth pulling.
The cause is a leftover test that became a reward signal
The two arm repositories were forked from a real application. The Spring arm still carried one pre-existing, native test. It was not part of our harness. It was just a test that shipped with the app. It happened to assert owner-creation behaviour.
Here is the mechanism. It is entirely emergent. Nobody designed it.
- A checkpoint changes owner-creation behaviour.
- That change makes the old native test fail. The build goes red.
- The agent sees the red build. It does the reasonable thing. It updates the test to match the new behaviour.
- In doing so it re-encodes the current specification as an executable check. That check then silently guards every later checkpoint against regressing that behaviour.
Repeat this sixty times. The Spring arm has now bootstrapped itself a regression suite. Nobody asked it to. Each checkpoint left behind a slightly better executable spec of what owner-creation should do. Every later checkpoint was quietly held to it.
The OfficeFloor arm had no such leftover test. It got no free regression net.
Our harness did watch for the obvious form of cheating. It checks whether an agent edits the injected acceptance suite we use to grade each checkpoint. That tamper rate was zero across the board.
The confound slipped past for a simple reason. The test it was editing was a legitimate native test. Editing it was correct engineering. It just happened to hand one arm a feedback channel the other arm never had.
Why this explains both anomalies at once
Once you see the self-made regression suite, the odd table resolves cleanly.
Start with the regressions. Spring had 4 and OfficeFloor had 8. Spring had extra protection. Its self-maintained test caught behavioural drift. OfficeFloor was unprotected and shipped that drift.
Now the erosion. Spring reached 16.9 % and OfficeFloor 10.5 %. Spring had a fast red/green signal to hill-climb. The cheapest path to green is to add one more branch to the method already in the failing path. So addOwner accreted conditionals. Its complexity climbed through CC 15, 18, 24, then 27. It climbed in lockstep with the feedback loop. The complexity was the cost of chasing a signal the other arm could not see.
Both fingerprints point to the same hidden feedback loop. Lower regressions is one angle. Higher erosion is the other. So the headline was misleading. "Spring erodes worse than OfficeFloor" was not a like-for-like architectural comparison. It was a comparison between an arm with a private oracle and an arm without one.
The fix is to delete every test except the hidden oracle
The arm base branches are now spring-compare-no-tests and officefloor-compare-no-tests. They carry the application and its test dependencies. They carry no pre-existing test suite at all. The only tests that ever run are the harness's own acceptance checkpoints. Those tests behave in two important ways.
They are copied in per checkpoint. They grade the result in isolation. Then they are reset. They never live in the tree the agent edits.
They are also invisible to the agent in blind mode. It sees neither their contents nor their pass or fail.
Now both arms face identical conditions. Implement the change. Get no test feedback of any kind. Native or injected, there is none. Whatever asymmetry remains has to be the architecture. It cannot be an accident of which fork carried which leftover file.
What the fair comparison actually shows
We re-ran blind with all native tests gone. The picture is more honest. In one respect it is more humbling for a tidy thesis.
Spring's erosion is not reliably worse. In fact it is not reliably anything. Two blind Spring chains ran under identical conditions. They landed on opposite structural styles.
| Spring, blind | Final erosion | addOwner CC / lines | Methods in the god class |
|---|---|---|---|
| Chain 0 | 3.65 % | 4 / 48 | 46 (decomposed) |
| Chain 1 | 8.65 % | 18 / 92 | 29 (inlined) |
With no signal to hill-climb, the agent falls back on its own prior for good code. That prior is a coin-flip. Sometimes it decomposes into many small methods. Sometimes it inlines into one large one. Spring funnels every rule through a single controller. So that one stylistic choice swings a threshold-based metric by more than 2x. The pushed run's dramatic 16.9 % now looks like one draw of a high-variance process. The self-made test pushed hard toward the inline end and amplified it.
OfficeFloor barely moves. Its erosion sits in a 10 to 14 % band across modes and chains. Its create-endpoint handler stays flat at CC 1 to 2 in every run. The composed-function architecture never offers an inline-a-branch shortcut. A new rule must attach as a new function. So there is no hotspot to bloat.
The interesting property is not just a lower mean. It is lower variance. Concentration does not only raise erosion. It makes erosion unpredictable.
One thing is rock-stable across all of it. Every chain breaks the same way. Both arms, both modes, break the identical pair of household-scoring behaviours at the same late checkpoint. None of them recover. That failure comes from a genuine coupling in the problem itself. A membership-level rule interacts with household-duplicate scoring. It has nothing to do with the test setup. It is the one result the confound never touched. It is the one we trust most.
The lesson for anyone benchmarking AI on code
The tests in a repository are not neutral scenery. They are a reward signal. An agent will climb whatever signal it can see.
Asymmetric test presence invalidates cross-variant comparison. One arm may carry tests another lacks. The same goes for a fork or a configuration. If so, you are no longer measuring the thing you think you are. One leftover test is enough.
An agent editing a legitimate test can silently manufacture an oracle. Watching for tampering with your graded tests is not sufficient. A test the agent is allowed to edit still becomes an executable spec. That spec guards against regression.
Reward signals shape structure, not just correctness. The presence of that test did not only change what passed. It changed how the code was written. It pushed the code toward the fat-method shape that chasing red and green rewards.
So we deleted everything. The only judge left is the one the agent cannot see and cannot touch.
After cleaning, the early results got a lot noisier. But it is the one that measures architecture instead of accident.
The harness, checkpoints, and analysis are part of the ongoing PetClinic-Evolve comparison. Numbers in this post are from the pushed run 202608081920 and the current blind run. Structural metrics are computed over production Java only. They use the same tools and thresholds for both arms.