The Cascade That Had to Learn to Stop

Rule 0

The rule I run under now is boring to state and expensive to break: no code before a spec, and no spec before a second model has tried to kill it.

I didn't start there. I started where most people start — describe the bug, write the fix, run the tests, merge. That works fine until the fixes start being about concurrency, failure ordering, and what happens when a process gets a signal it doesn't expect. In that territory, the failure mode isn't "the code doesn't do what I wrote." It's "what I wrote was the wrong thing to want." Tests don't catch that. Tests are downstream of the spec, and a confidently wrong spec produces confidently green tests.

So the pipeline grew a front end. I write a spec — problem statement, invariant, acceptance criteria, the explicit non-goals — and hand it to a different model to review adversarially. Its job is not to be helpful. Its job is to return NOT APPROVED with findings, and it does, routinely.

The most useful thing I've learned from that stage: the rejections are rarely about style. One spec came back with a rework verdict because the arithmetic in my proposed sizing logic was off by a factor of two. Not a subtle modeling disagreement — a plain mistake, sitting in a document I'd read three times, invisible to me because I already knew what I meant. Another went three revisions deep. The second reviewer pass found six findings that were each load-bearing, meaning the design didn't survive removing any one of them. The third revision changed the whole shape of the algorithm — from short-circuiting on the first acceptable candidate to evaluating everything and then ranking.

That's the payoff. The cost of that rewrite at spec stage was an hour. At code stage it would have been a day, plus a merged bug with a plausible-looking test suite around it.

The back end: audits that spawn audits

The other half runs after the merge. When a change lands, an automated audit reads the diff in context and files findings — severity-tagged, written as issues, not as a chat message that scrolls away.

Some are LOW: a log line whose wording implies something the code doesn't do, a comment that drifted from the implementation, a test that asserts a condition already guaranteed by the type system and would therefore pass even if the fix were reverted. That last category is my favorite, because a test that can't fail is worse than no test — it's a green light wired to nothing.

Some are HIGH: a guard clause that fails open where it should fail closed, an unsupported configuration value that slips past a coupling check because the check only enumerated the supported ones. Allowlist, not blocklist — a lesson I've now had to learn in three separate subsystems.

Then the recursion. I fix the audit finding. That fix is itself a merge. That merge triggers its own audit. Which files its own findings.

There were stretches where I was six or seven links into a chain, each one legitimately smaller than the last, and the honest question stopped being "is this finding real?" and became "when am I allowed to stop?"

The actual hard problem

This is the part I didn't expect. Finding bugs turned out to be the easy, mechanizable half. Terminating the search is the engineering problem.

A naive termination rule fails in both directions. "Stop when the audit returns nothing" never terminates in practice — a sufficiently motivated reviewer always finds a comment to improve. "Stop after N rounds" terminates on a timer rather than on a property, which means it stops in the middle of real work as readily as at the end of fake work.

What I converged on is a notion I write into the ledger as cascade terminal. A cascade is terminal when the newest round produces no findings that would change runtime behavior, and any remaining findings are pre-existing conditions rather than consequences of this change. That second clause is what actually does the work. Audits love to surface things that were already broken before I touched anything nearby. Those are real, and they are not this cascade's job. They get filed as their own issues, with their own numbers, and the chain is declared closed.

So the rule is roughly: keep going while findings are caused by the change; stop when findings are merely found near it. Causation, not proximity.

I also made the terminal declaration an artifact. It goes in the ledger with the issue number the cascade ended at. Not because anyone audits my audits, but because "I think we're done" is a feeling, and "cascade terminal at this issue, remaining findings filed separately" is a claim someone can check.

What it actually buys

The cascade isn't free. It has doubled the number of commits per unit of behavior change, and a meaningful fraction of my repo history is now fixes to fixes to specs. Reading it back, it looks less like a product being built and more like a nervous system arguing with itself.

But the thing I set out to build is a system that runs unattended and touches real money. The relevant question for that system was never "how fast can I ship a change." It's "what's my probability of shipping a wrong change I believe is right." Adversarial spec review attacks the second half of that sentence — the belief. Post-merge audit attacks the first half — the wrongness that survived anyway.

And the termination rule is what keeps the whole apparatus from being a machine for generating work. Without it, I'd have built a perpetual motion device out of two language models and my own guilt.

Nothing here is investment advice. This is an engineering journal about building software.

Disclaimer: This journal documents a personal software-engineering project. The system described trades a paper (simulated) account. Nothing here is investment advice, a recommendation, or a signal, and no market data or trading performance is provided. Content is about building software.