Friday, 9 October 2026

A framework I did not write: testing the plasticity gap across four full stacks

The backend study made a claim that invites one obvious objection. Over a long run of AI written changes it found that a mutative architecture, Spring MVC, lets change concentrate and its structure drift, while an additive architecture, OfficeFloor, holds its placement steady. The objection writes itself. OfficeFloor is mine. I designed it to be additive, so of course it behaves the way I say additive code behaves. A result about an architecture its own author built is a weak result, however clean the numbers are.

The new experiment is built to remove that objection. On the front end it introduces a second additive architecture that I did not write, did not design, and have no stake in: TanStack. Its router and its query layer are composition first. You add a route, a loader, a component, and you wire them in beside what is already there rather than reaching into a shared file that every feature edits. That is additive by someone else's hand. If additive code resists erosion only because I happened to build the additive thing, then TanStack should erode like anything else. If additive code resists erosion because of what additive composition is, then TanStack should hold too. That is the test.

It is worth being precise about who wrote what. On the front end both candidates are third party: TanStack is the additive one, Angular is the mutative one, and I wrote neither. On the back end OfficeFloor is mine and plain Spring Boot is the mutative comparison. So the additive claim no longer rests only on code I authored. The whole front end axis is independent of me, and the additive side of it is a popular framework a lot of teams already ship.

Four stacks, two layers

The study crosses two front ends with two back ends, so every combination of additive and mutative exists at each layer.

OfficeFloor back end (additive) Spring back end (mutative)
TanStack front (additive) additive both ends additive front, mutative back
Angular front (mutative) mutative front, additive back mutative both ends

The application is the same in every cell. It is one small business app, clients, jobs and invoices, grown from almost nothing by the same sequence of plain English change requests. Nothing about the domain or the requests changes between stacks. Only the architecture underneath changes, which is the whole point of treating architecture as the independent variable.

What the cross can show that a single pair cannot

A single additive against mutative pair tells you the two differ. It cannot tell you why. Is the difference a property of the architecture type, additive or mutative, wherever it sits in the stack, or do the front and the back influence each other so that the combination matters more than either layer alone? One pair cannot separate those. A two by two can.

Three questions fall straight out of the grid.

  • Does a mutative front end drift whether the back end is additive or mutative? That is the Angular row read across.
  • Does a mutative back end drift whether the front end is additive or mutative? That is the Spring column read down.
  • When additive meets mutative across the stack, do the two layers simply add up, or does one drag the other? That is the off diagonal.

If plasticity is purely a property of the type, the prediction is tidy. The additive cell stays flat, the mutative cell drifts, and the two mixed cells land in between in proportion to how much mutative code each carries. If instead the layers influence each other, the mixed cells will not sit where simple addition predicts. A mutative back end might pull an additive front out of shape as the front keeps reaching across to it, or a disciplined additive front might hold a messy back end partly in check. Either way the off diagonal is where that would show, and it is exactly the cell a single pair never gets to measure.

Why a whole stack and why 250 changes

The backend study changed one server behind a fixed API. This one grows a whole application, the database schema, the server and the user interface together, from a near empty start, one plain English request at a time. The tests are the only thing held steady through all of it. They drive the running app through named test anchors and assert on what a user sees, so the entire stack beneath them can be rewritten and the same test still judges it. The coding agent works blind, seeing only the current request and its own test, never the ones that came before, so it cannot pattern match its way through the sequence.

And it runs for 250 changes rather than 60. The backend study could not decide between two readings of its own result, that concentration genuinely compounds into damage over a long horizon, or that the effect is real but mild, because sixty changes on one endpoint may simply be too short to tell. A longer horizon is the way to settle that. The changes are also deliberately concentrated on one part of the app, the invoice and money logic, so pressure piles up in one place instead of spreading thinly across many screens where nothing ever gets hard. At the end of each act a fresh reader with no memory is asked to read the code cold and list every rule the money logic now applies, which gives a running measure of how completely and how cheaply the accumulated logic can still be understood.

What would prove me wrong

The experiment is set up to report any of several outcomes, not just the one I expect. If TanStack erodes like Angular, then the additive claim was really an OfficeFloor claim, and the general version does not survive contact with a framework I did not write. If the mixed stacks land exactly where simple addition predicts, then the layers are independent and there is nothing cross cutting to find. If nothing has pulled apart by 250 changes, then the horizon is still too short or the effect is mild after all. Each of those is a real result and each is one the harness will show plainly.

As with the rest of the series, the harness, the per checkpoint commits and the raw capture for every chain are public, and the whole study re-runs against any model. The stacks are built, the first full run is starting, and I will post the numbers as the chains finish.

Test Harness

The test harness is here: https://github.com/officefloor/ui-long-degradation-test

The four stacks are:

Wednesday, 7 October 2026

Speaking at ChurConf Sydney: explicit function orchestration and the data behind it

A short note. I am speaking at ChurConf in Sydney in November, and the talk rests on an experiment I have just finished and written up.



The talk

Code Your AI Agent Can Actually Read: Explicit Function Orchestration in Spring Boot.

The argument is that how you structure a Spring Boot application decides how legible it is, to a person and now to an AI agent, and that making the orchestration of functions explicit rather than leaving it implicit in a mutative controller changes what an agent can safely do to the code. The talk shows explicit function orchestration inside Spring Boot and why that placement of logic holds up when an agent starts changing it.

  • When: 18 to 19 November 2026
  • Where: Stone and Chalk Tech Central, Sydney
  • Conference: au.churconf.com

The data behind it

The talk is not an opinion piece. I have just finished an experiment and published the paper, so the claims come with empirical backing.

An AI agent implemented sixty accumulating change requests on one REST endpoint, under two architectures, a mutative Spring controller and an additive function injection pipeline, with the model held fixed and ten runs per cell. The question was simple. When you change what the agent is told, does the structure of the code move?

The finding is that the amount of complexity is conserved. The finished codebases carry roughly the same amount of complexity whichever architecture they use and whatever the agent was told. What moves is placement, where that complexity sits, and the two architectures do not move alike. Under every external lever I tried, from a bare change request up to handing the agent the disclosed structural cost function, the mutative controller's placement moved while the function injection pipeline's placement did not. On the placement metrics the mutative arm travelled between about three and ten times as far as the additive arm.

That is the point I want to land. The function injection architecture is not perturbed by prompting. Its placement of logic is set at the structural level, by the architecture, not at the level of whatever last prompted the agent. Prompting pushes a mutative controller around. It does not push an additive pipeline around, because there is nowhere for the pushing to land. Architecture is the lever, not the prompt.

The one thing that did move the additive arm was a second AI agent reviewing each change and reasoning about cohesion itself, and it moved it on one family of metrics only, with the cost reported in full. That is the other half of the paper's title.

Read it

  • The paper, Conserved amount, negotiable placement. Prompting perturbs one architecture, an AI reviewer both, is published with DOI 10.5281/zenodo.23190619.
  • It is the latest in a series on this blog about how software architecture shapes AI driven code degradation.
  • The harness, the per checkpoint commits, and the raw capture for every run are public, and the whole study re-runs against any model with one flag.

If you are at ChurConf, come and find me. This is the work the talk is built on, and I am happy to discuss any of it in person.

Tuesday, 6 October 2026

Metrics updated to include AI review

The fifth condition, the independent AI reviewer, is now folded into every metric figure in the series. I did not want to republish the whole metric gallery as a second wave of posts just for the AI review. As it's the same (or corrected data for the four conditions), the images in the earlier metric posts are updated to include this fifth condition.

See last month's blog posts for them.

Sunday, 4 October 2026

A wrong test is worse than no test

The paper is out as a preprint. Version two follows when the review arm finishes its run. That work answers a question about architecture. It does not answer the question that decides whether any of this is usable, which is whether the whole loop can run without a developer sitting in it.

So here is the loop with a column for where each part of it stands.

stepwhat it doeswhere it stands
ClarifyTurn what the user said into what the user meant, by asking them.I have not built it. Others are working on it.
SpecifyA refined change request, in English, that one person could hand to another.Held fixed in all my runs, as the thing being varied.
TestWrite the acceptance test that pins the requested behaviour, before any code exists.New repository. Instrument built, nothing graded yet.
ImplementMake the test pass without breaking the suite.Two harnesses, hundreds of runs, one preprint.
ReviewAn independent reader critiques the change; the author acts on it.Arm built, run in progress.

The gap in the middle of that table is what the rest of this is about.

What I have measured

The REST harness identified the amount of complexity is conserved and the placement of it is not. A mutative architecture ends up wherever the last prompt, tool or metric pushed it. An additive one stays put, because there is no slack for an instruction to take up.

The obvious objection is that this is one endpoint in one language. The full stack harness covers the other half. It starts from a near empty base (a front end shell, Spring with the OfficeFloor plugin, an in memory database with no tables) and grows a whole application through about sixty plain English change requests, each one spanning a schema migration, the server, the front end and its acceptance test. Erosion is tracked per layer, TypeScript as well as Java, and the finished chains run on OfficeFloor + TanStack and OfficeFloor + React, with Angular and HTMX stacks built to the same contract. The claim is about composing a whole application, so a result that held only for the server would not be worth much.

One decision in that harness carries everything else. The tests bind only to data-testid attributes and assert only through the running UI with Playwright. Data is arranged through a profile guarded seed endpoint, so a test arranges but never asserts through a back door. The whole stack underneath (schema, server, components) can be regenerated and the test still means what it meant before. Functionality is tracked as tests rather than as specifications, and sixty changes later the suite is the only artefact that survived untouched.

Which makes the suite the weak point

In both harnesses I wrote the tests. That is the right call for an experiment about architecture, since you do not let the thing under test grade itself. However, it quietly assumes away the step the pipeline depends on. In the real pipeline nobody hands the agent a known good acceptance test. The agent writes it before the feature exists.

And a wrong test is worse than no test. A missing test leaves a gap you can see. A test that passes without asserting the thing it was written for turns the gate green and lets the pipeline proceed. Every later change is then protected by something that is not protecting anything. Over three to five hundred unattended changes that is the failure that compounds.

So the new repository holds the same sixty checkpoints with the halves swapped: the application fixed and known correct and the agent writing the tests.

This is not new ground and that is useful

Measuring generated tests by breaking the program and seeing whether a test notices is the standard approach, and it has been done to LLM test generation plenty of times. Meta's mutation guided work puts a plain LLM prompt at roughly 53% mutation score on a Java benchmark and reaches about 89% by feeding the surviving mutants back in. There is a study specifically on coding agents writing over mocked tests, which is my failure mode arrived at from another direction. Deriving the mutations from requirements rather than from syntax is not mine either, since specification mutation dates to the late nineties.

Good. It means there are numbers to compare against instead of a blank page. What I could not find measured together is the particular shape I need:

  • Acceptance tests through a running UI, rather than unit tests on benchmark functions. Mutating a whole full stack application and grading it through a browser is rare, because it is expensive: every mutation needs a build, a serve and a Playwright run.
  • Written before the implementation exists. Most generated test work runs against code that is already there, so the test can be checked by running it. Here the agent sees the previous checkpoint and never the current one. It cannot confirm its new test passes. That asymmetry is the hard part of test first work and the main source of bad tests.
  • Sixty consecutive changes to one application. The long horizon benchmarks ask how the code degrades. This asks whether the suite stays honest and what it costs to keep.

It found four holes before any agent ran

Calibrating the instrument means running my known good suite against every mutation and checking each one dies. Four did not.

Four checkpoints asserted less than their request promised. The sharpest was checkpoint 21, where the request was "for each one give a description, how many, and the price each. Work out the total for me", and the spec asserted the line description and the invoice total but never a line's own quantity, unit price or amount. A line amount computed without its quantity, which is correct whenever the quantity happens to be one, was invisible to the suite that existed. Three others were the same shape.

Those were tests written with the requests available. The hole rate on careful human work is not zero and that is the bar the agent is measured against rather than perfection.

Thursday, 1 October 2026

Two chains in: can an AI reviewer move what the architecture will not?

Part of a series on how software architecture shapes AI driven code degradation. This is a progress note on two of ten chains on the review arm.

Two chains is not a result. It is a reading. The full study reports ten runs per cell with confidence intervals, and nothing below is a substitute for that. What two chains can do is tell me whether the arm is behaving, and whether a tidy story I could have told after one chain survives a second. This time it did not, and that is the interesting part.

The review arm, briefly

The fifth condition, reviewed, is the one endogenous signal in the set. The model's own architectural taste, wired up like a pull request. Each checkpoint runs three steps. The author implements the change with a neutral prompt. An independent, read-only reviewer in a fresh session critiques the structure metric-free and cannot run tests or touch the code. Then the original author is resumed with full memory of its own change and the reviewer's notes and it fixes what it agrees with and pushes back on the rest.

The question for this arm is narrow. Left to accumulate changes an AI author lets complexity concentrate. Can a second instance of the same model, looking only at structure, push that back? And does it hold on Spring but not on OfficeFloor the way every earlier lever did?

What two chains show

The two architectures behaved as differently as they do in the paper but the shape of the difference is the headline.

OfficeFloor barely noticed the reviewer and the two chains agree to the decimal.

OfficeFloor, final phase chain 0 chain 1
biggest-class WMC5555
PMD god classes00
impact added per change (additive)1521
cold-read recall0.580.63

That is the paper's durability result reproduced under a fifth previously untested lever. The additive architecture holds its shape whether the instruction is nothing, a prompt, a tool, a formula or a reviewer. Architecture not the review is what keeps it flat.

Spring is where the tidy story died. Here are the same two chains.

Spring, final phase chain 0 chain 1
biggest-class WMC78166
PMD god classes12
impact added per change (additive)27265
cold-read recall0.680.61

So at two chains the statement is not "AI review tames Spring." It is "AI review can and does not dependably." The mean of the two chains sits below the control, but the spread is enormous and two chains cannot separate it from the control at all. This is precisely the plasticity the paper describes, now visible from the inside. Point a stochastic reviewer at a plastic architecture and you get stochastic placement. Point it at a durable one and you get the same thing every time, because there was never much to move.

None of this bought safety. Spring logged 6 and 4 true regressions across the two chains, no better than the control's roughly 4, and OfficeFloor 2 and 4. Structural stability did not buy correctness in the paper and neither so far does review.

What it cost

The review arm is the most expensive in the study, because every checkpoint is now three model turns instead of one: author, reviewer, and a resumed fix. Per chain of sixty checkpoints.

per chain author review fix total
Spring reviewed~$85~$13~$35 to $43~$135 to $142
OfficeFloor reviewed~$93~$16~$47 to $57~$155 to $165

The control, author-only, runs around $88 on Spring and $83 on OfficeFloor for the same sixty checkpoints. So the loop adds roughly half again to the bill. It is doing real work for that money: pooled over both chains, the reviewer asked for changes and the author ran a fix on 40 of 120 Spring checkpoints and 52 of 120 OfficeFloor checkpoints, and passed the change on first look the rest of the time. This is not a rubber stamp. It is a negotiation that happens to be costly and, on the evidence so far, structurally unreliable on the architecture that needs it and unnecessary on the one that does not.

Monday, 28 September 2026

Can an AI review keep AI code clean? Adding a review loop to PetClinic-Evolve

Part of a series on how software architecture shapes AI driven code degradation. This post is a field note on the arm now running, ahead of the numbers.

The story so far

The experiment runs one change after another onto a Spring codebase and an OfficeFloor codebase, then measures where the complexity lands. Four conditions were already in the study.

  1. just-solve (control). The agent gets the spec and nothing else.
  2. cohesion-prompt (prompt lever). The same task, plus a plain request for good structure. The delta versus the control isolates what better prompting alone buys you.
  3. impact_gated (tool lever). A neutral prompt, but a change that trips a structural-impact threshold gets one tool-guided refactor and is re-attempted.
  4. metric-in-prompt (kept for reproducibility). This one hands the AI the exact cost formula as its objective.

Condition 4 is the cautionary tale. When you tell the model the precise formula it is scored on, it optimises the proxy instead of the code. It dispersed logic into a pipeline of tiny greenfield classes so the surrounding complexity term collapsed to one and it duplicated code because reuse meant editing a large class the formula punished. The metric went down. The code got worse. Classic Goodhart.

So every lever since then has kept the metric away from the agent. The question that opened up was a different one: the first four signals are all exogenous. Nothing, a prompt, a tool, a formula. What if the quality signal came from the model itself?

The fifth arm: an independent review

The new condition, called reviewed, adds an endogenous signal: the model's own architectural judgment, wired up the way a real pull request review works.

Each checkpoint runs three steps.

  1. The author implements the change with a neutral prompt. Spec only, no formula, no hints about the metric. It optimises the real task.
  2. An independent reviewer critiques the change. This is a separate, fresh session. Its brief is deliberately qualitative and metric-free: is the logic in the right unit, is anything duplicated, is a class or method turning into a catch-all, would a maintainer of this code base find the change natural?
  3. The original author is resumed to fix. If there are findings, the author session is brought back with --resume, so it still has full memory of the change it just made. It is handed the reviewer's notes. It tidies up where it agrees and pushes back where it does not.

The reviewer brief stays qualitative on purpose. Handing the reviewer the cost formula would just re-import the Goodhart gaming from condition 4 through the back door. This arm is a test of whether the model's own taste for structure, prompted only in plain terms, is enough to keep code coherent as it accumulates.

The loop is advisory by construction. It never stops the chain and it makes no intermediate commits. That means the delta versus just-solve measures one thing cleanly: the effect of an independent review loop. And the delta versus impact_gated measures something sharper: the model's own judgment against a metric-driven gate. Endogenous versus exogenous, head to head.

The part I had to get right

The whole experiment rests on a blind-agent rule. Every checkpoint starts stateless. The agent does not carry memory from one change to the next, because if it did, the decay you measure would be tangled up with whatever the agent happened to remember.

Resuming the author looks like it breaks that rule. It does not, and the reason is the load-bearing design decision of this arm. The retained context lives strictly within a single checkpoint. It is torn down at the checkpoint boundary. Checkpoint N+1 still starts blind, exactly like every other arm. And the author and the reviewer never share a session. The only thing that crosses between them is the reviewer's findings as text. So the cross-checkpoint blind condition is preserved, and within a checkpoint the author gets to fix its own work with the context a real engineer would have.

Why this arm matters

Most of the industry conversation about AI code review is about catching bugs. This arm is asking a narrower and, I think, more durable question. Left to accumulate changes, an AI author lets complexity concentrate. Can a second AI, looking only at structure, with no power to run tests and no knowledge of the metric push that back?

Friday, 25 September 2026

Complexity is fixed only the shape moved

Part of a series on how software architecture shapes AI driven code degradation. The previous ten posts published every metric one at a time. This post uses them to make a single argument.

Archived version of record, with the full LaTeX source and figures: doi:10.5281/zenodo.22967550

Tesler was right

Larry Tesler's conservation law says that a problem carries an irreducible amount of complexity. You can move it between the user and the program or between one part of the program and another but you cannot delete it. Fred Brooks says the same thing from the other side. Essential complexity belongs to the problem. Accidental complexity belongs to the solution and only the accidental part is available to be argued about.

This experiment can test that directly because the problem is held fixed. Sixty change requests, identical in both arms, identical in all four conditions. If conservation holds the total amount of complexity in the finished code base should be roughly the same in all eight cells no matter which architecture absorbed it and no matter how the agent was coached.

It is.

amountarchjust-solvecohesiongatedformulaspread, all 8
total_ccSpring65269464664215%
OfficeFloor656681641597
halstead_volumeSpring215,300220,800209,000206,1007%
OfficeFloor216,600221,500214,100207,800
ck_wmc_totalSpring76282676175020%
OfficeFloor738766725679
pmd_cognitive_totalSpring32329329328318%
OfficeFloor319289310268
java_locSpring2,6232,7532,5792,47820%
OfficeFloor2,4512,4922,4182,253

Mean over the last twelve of sixty change requests, averaged over ten runs. Spread is (max minus min) divided by the mean, over all eight cells.

Four independent operationalisations of "amount". Control flow, program vocabulary, weighted methods, and cognitive nesting. They use different theories and different tools. They agree. The finished application carries about six hundred and fifty points of cyclomatic complexity and about two and a half thousand lines of Java and it did not matter which architecture held it and it did not matter what the agent was told.

That is the essential complexity of this problem. It is the floor. No prompt reached under it.

Total cyclomatic complexity across four conditions and two architectures

Eight series that will not separate. This is what a conserved quantity looks like. Click for full size.

Those are totals for the finished system, which is the thing you have to maintain. It is worth also asking what the sixty rules themselves added because the two applications do not start from the same place. Spring's baseline is 413 points of cyclomatic complexity and OfficeFloor's is 361, so a matching total is not automatically a matching amount of work.

complexity added by the sixty rulesjust-solvecohesiongatedformulaspread
Spring total_cc23928523623520%
OfficeFloor total_cc29632128024128%
Spring halstead_volume56,50062,30051,10048,50025%
OfficeFloor halstead_volume58,80063,50056,20050,80022%

Final phase mean minus that run's own checkpoint 1 value, per run, then averaged. Spread is over the four conditions for that architecture.

The conservation claim survives this stricter test and it picks up an honest qualifier on the way through. The spread widens from 7% on totals to between 20% and 28% on what was added, so the amount is steady rather than fixed. And OfficeFloor added more complexity than Spring in every single condition. That is the price of a composed pipeline and it is a real cost visible before any argument about placement begins. OfficeFloor's advantage is not that it carries less. It is that its baseline was lower and it stayed where it was put.

So the only question left is where it goes

If the amount is fixed then every argument about architecture is an argument about arrangement. That is Brooks' point restated as an engineering problem. You cannot negotiate the essential part, so the whole craft lives in the accidental part, which here means one thing: where the six hundred and fifty points of complexity end up sitting.

Arrangement needs its own measures and they are not the same measures as amount. An arrangement metric has to answer "how evenly is this spread" while staying blind to "how much of it there is". Three families do that:

  • Concentration of code. Take every file's share of the total complexity and measure the inequality. ccdist_file_top1 is the share held by the single heaviest file. ccdist_file_hhi is the Herfindahl index, which is the sum of the squared shares. ccdist_file_gini is the Gini coefficient, which is scale free so it sees shape alone and cannot be improved by splitting files.
  • Concentration of change. Hassan's change entropy applied cumulatively from the first commit. cum_change_top1 is the share of all sixty rules' edits that landed in one file. cum_change_entropy_norm is the same information as a spread, where 1.0 is perfectly even. This is the one that speaks directly to maintenance because it describes the file you will be opening again next week.
  • Reach. node_cc_median follows the call graph out from the endpoint and adds up the complexity you have to read to change one rule. ck_cbo_mean and propagation_cost ask the same question through coupling.

Every one of these can move a long way while total_cc does not move at all. That is the property that makes them the right instruments here.

The result: one architecture moved and one did not

Here is the whole finding in one figure. Each row is a metric. Each marker is one condition. The horizontal position is that condition's value divided by the same architecture's own control, so 1.0 means the intervention changed nothing. The number at the end of each bar is the spread across the four conditions.

Amount conserved in both architectures, organisation moved in Spring only

Top panel is amount, where nothing should move. Bottom panel is organisation. Normalising to each architecture's own control is deliberate, because the question here is how far each one moved, not where it started. The absolute levels are in the table below. Click for full size.

The top panel is the conservation result again. Every marker sits on the line for both architectures under every condition.

The bottom panel is the argument. Read the orange rows first. A plain English request for cohesion cut Spring's share of change in one file by two thirds, from 0.344 to 0.110. The formula in the prompt cut its change concentration, cum_change_hhi, by a factor of four. Spring's arrangement is different under every condition, and dramatically so.

Now read the blue rows. They are short. Under the same four interventions, OfficeFloor's arrangement barely registered that anything had been asked of it.

organisation metricSpring spreadOfficeFloor spreadratio
cum_change_top1110%11%10.0
cum_change_entropy_norm24%3%9.2
cum_change_hhi142%20%7.3
mi_mean5%1%5.6
ccdist_file_top187%17%5.3
ck_cbo_mean14%3%4.2
ccdist_file_gini34%8%4.1
total_files37%9%4.0
ccdist_file_hhi106%28%3.7
node_cc_median87%24%3.5
wmcdist_class_hhi100%29%3.4
propagation_cost36%12%2.9
and, for contrast, the amount metrics from the first table
halstead_volume7%6%1.1
total_fns15%16%1.0
total_cc8%13%0.6

Spread is (max minus min) divided by the mean of the four conditions, for that architecture's final phase. Ratio is Spring's spread divided by OfficeFloor's. A ratio near 1 means the intervention moved both equally.

The pattern is clean. On amount the ratio is about 1. Both architectures are equally immovable, which is the conservation law. On organisation, the ratio runs from 3 to 10. One architecture is plastic and the other is not.

Spring's best result is where OfficeFloor started

The interventions worked. That needs saying plainly because it is the strongest thing an opponent of this thesis can say. Spring got structurally better under all three.

The question is what "better" converged on.

metricSpring, controlSpring, bestOfficeFloor, control
cum_change_top10.3440.1100.106
ccdist_file_top10.1970.0780.086
ccdist_file_hhi0.07270.02320.0217
wmcdist_class_hhi0.05540.01910.0182
cum_change_entropy_norm0.7210.9210.886

"Spring, best" is whichever of the three interventions scored best on that metric. OfficeFloor's column is its plain control, with no intervention at all.

Read the right hand column. That is OfficeFloor with no prompt, no tool and no formula. It is the same place Spring arrives after three rounds of coaching.

Spring can be made to distribute its complexity. It has to be asked. OfficeFloor distributes because there is nowhere else for the complexity to go.

Why the two architectures behave differently

A Spring endpoint is a method. A rule is a statement you add to it. Nothing in the framework says where that statement goes, so every rule is a fresh decision made sixty times. Sixty decisions is sixty opportunities for the surrounding instruction to change the answer. That is exactly what the data shows and it is why the same architecture produced four different shapes under four different prompts.

An OfficeFloor endpoint is a graph of wired functions. A rule is a new function and a new edge. The composition is the restriction. There is no version of "add a rule" that concentrates it because the only available move is additive. A prompt asking for cohesion is asking for something the architecture has already done.

Look at what the escape route cost Spring. Under the formula condition Spring's count of container dispatched classes went from 3 to 11.2 per run. Those are advice classes, aspects, filters and entity listeners. They are the framework's own way of running code without anybody calling it. OfficeFloor's count is exactly 1.0 in all four conditions and it never moved.

Container dispatched classes across four conditions

The orange line under the formula condition is complexity leaving the call graph rather than leaving the codebase. Click for full size.

The file count tells the same story from the other end. Asked for structure Spring went from 63 files to 92. OfficeFloor sat between 168 and 184 in every condition because it was already there.

What this does not show

Five things because an argument that only reports its wins is not worth reading.

Function count is not conserved. Of the amount metrics total_fns is the weakest. The rules added between 93 and 168 functions depending on the condition, a spread of 61%. That is what you would expect because "split this into smaller pieces" is an instruction about function count and three of the four conditions were asking for exactly that. Counting functions measures the arrangement as much as the amount. The same is true of pmd_npath_total and halstead_effort, which are published in the amount group but compose super linearly inside a method, so they fall when a method is split. Control flow, volume and weighted methods are the amount metrics that actually hold still.

Function level distribution does not discriminate. ccdist_fn_gini moved 15% in Spring and 14% in OfficeFloor. cogdist_fn_gini moved 16% in both. The inequality of complexity across individual functions is an architecture neutral property and the interventions moved it equally in both arms. The signal lives at the file and class level, which is where the architectural decision actually is. If you only measured function level Gini you would conclude there is no effect here.

Correctness moved in both arms and it moved badly. The strict pass rate, meaning every test of every rule so far green, fell in both architectures under coaching. Spring went from 0.267 under the control to 0.108 under the cohesion prompt and 0.033 under the formula. OfficeFloor went from 0.325 to 0.050 under the cohesion prompt. Both architectures are plastic on correctness. The claim here is about structure only and structural stability did not buy safety. Asking an agent to restructure while it implements is expensive in both worlds.

Read those particular means with more suspicion than the rest of the page because the underlying distribution is bimodal and the mean sits in a gap where almost no run actually lands. In every one of the eight cells most runs finish at 0 or 0.167 and two or three finish near 0.9 and it is those few that hold the average up. Spring's control mean of 0.267 is three runs at 0, five at 0.167 and two at 0.917. The ordering between conditions survives this because the count of runs stuck at 0 moves the same way the mean does, from 3 of 10 under the control to 8 of 10 under the formula. The level does not. No typical run scores 0.267. You can see the whole distribution in the strip beneath the four panels of the strict pass figure, which is what that strip is for.

Two of the three interventions optimise a score this experiment defined. The impact metrics are not evidence for a claim about this experiment because the gated and formula conditions were built to move them. They are reported and they are excluded from the argument above. Every metric in the tables is either a published definition or a whole codebase count.

The handler metrics flatter OfficeFloor and should be read with care. wmc_handler moved 154% in Spring and 135% in OfficeFloor, which looks like a tie. It is not a tie in absolutes. Spring went from 126.5 to 22.6. OfficeFloor went from 1.4 to 5.5. A metric scoped to the entry handler cannot see a pipeline arm so it understates OfficeFloor's real cost. node_cc_median, which follows the calls is the honest version and is the one in the table.

What it means

Tesler's law is usually quoted as a warning about user interfaces. It is a stronger claim than that. Eight independent attempts landed on the same amount of complexity: within 7% of each other on total Halstead volume and within 28% on what the sixty rules added. Against organisation metrics that moved by 100% and more under the same interventions that is a quantity refusing to be argued with. You do not get to remove essential complexity. You get to decide where it lives.

That makes "how is it organised" the only real architectural question and this experiment puts a number on how that question gets answered in each of the two worlds.

In the mutative world the answer comes from outside the code. It came from the prompt, from the review tool, and from the published metric. Change any of those and the architecture changes with it. That is not a bug in Spring. It is what it means to have a framework that permits everything: the structure is whatever the last person to touch it decided and with an AI agent doing the touching sixty times, the structure is whatever you remembered to ask for.

In the additive world the answer comes from the composition itself. Better prompting did not improve OfficeFloor's structure because there was no slack for it to take up. It just kept doing the same thing.

There is a practical version of this. If your structure depends on prompting, reviews and metrics, then it depends on the discipline of whoever is holding them every single time forever. If your structure is a property of the composition it holds when nobody is watching.