Sprint 47 — the fix was never only where I put it
Sixty-three PRs merged between August 24th and 31st. Three production releases. No database migrations at all. But the number I keep coming back to is six — the number of times, on a single pull request, that I fixed exactly what a reviewer told me to fix and was exactly as wrong afterwards.
The reviewer changed jobs without telling me
Every pull request in this repo gets reviewed by an AI model from a different family than the one that wrote the code. That was a deliberate choice: the app is written entirely by Claude, so a Claude reviewer shares the author’s blind spots. Last sprint was about earning that reviewer its place — measuring it against deliberately planted defects before trusting a word it said.
This sprint it did something I did not design for.
The change was authorization hardening on a two-step confirm flow: an assistant asks to do something, gets back a proposal, and confirms it in a second call. The security question is what happens in the gap. Between the propose and the confirm, a user might revoke the assistant’s access — and the confirm has to notice.
Round eight found a real bug: under contention, a proposal that was still perfectly valid could be marked expired and destroyed. I fixed it.
Round nine found that the same lie was reachable through a different door two hundred lines earlier. I fixed that.
Round ten found that a related rule — write the audit record before you change the row, never after — was violated in the same function. I fixed it, and said so in the commit message with some confidence.
Round twelve found that my round-ten fix had reopened its own hole, because I computed a deadline using the application’s clock and enforced it using the database’s.
Round eleven, out of order because that is how it arrived, found yet another door.
Every one of those findings was correct. Every fix I made was correct. And every fix addressed precisely the line number the finding named, while I never once turned around and asked where else in the file that same shape lived.
The rule, not the finding
What broke the loop was not the reviewer. It was the security station — another agent, whose whole job is to look at authorization changes before they land — and it did not give me a thirteenth finding. It gave me a rule:
When a finding names a line, grep the shape, not the line.
I ran it as a single command across the whole codebase. Two candidates. One a false positive that the code already explained — a tenant row genuinely has to exist before an audit row can reference it. The other was real, and worse than anything the twelve rounds had raised: creating a proposal recorded its audit entry after writing the row.
That one matters more, not less. Expiring a proposal takes a capability away, and the row’s own data proves it happened even if the audit is lost. Creating one grants a capability. It is the exact case the convention exists for, and it had been sitting there through twelve rounds of review because nobody, me included, had looked for the shape.
There is a lesson about AI review in here that I did not expect to learn. The reviewer’s accuracy was never the problem — it was right essentially every time. The problem is that a reviewer which names a location trains you to fix locations. You start operating like a ticket queue. The countermeasure is entirely on the author’s side, and no amount of reviewer quality supplies it.
Five bugs that had been fixed for a week, and were not
The same shape, arriving from a different direction.
Six days ago I closed five crash reports as one root cause. Domi’s database driver keeps a pool of connections; when the database provider restarts a machine, it severs every open socket. Node’s rule is that an error event with nobody listening does not get logged — it takes the process down. The fix was to attach a listener. Shipped, five issues closed, done.
This week one of them fired again. I checked which release it happened on, and that release contained the fix.
The listener was real. It was attached in the function that most of the app uses to reach the database — and that function was never the only place a connection pool gets created. A second one, in the code that resolves which household you belong to, built its own. That pool is cached for the life of the server process, which means its connections sit idle between requests, which makes them precisely the ones a restart kills.
One cause, five bug reports, all blaming unrelated pages — because when the process dies, the crash reporter blames whichever request happened to be running.
I fixed it at the constructor this time rather than the call site: one function that creates pools, and a lint rule that refuses to compile any other way of making one. Then the reviewer found three more pool sites in a directory my search had missed entirely. Same lesson, third arrival in one week.
One more thing worth saying plainly: the GitHub issues still displayed “last seen: August 22nd.” That is a snapshot from the moment the issue was filed, not a live value. Reading it instead of the actual error tracker would have closed five live bugs as fixed, for the second time.
A test that refused to lie
My favourite thing this sprint is small.
I wrote a browser test to prove that a filter on the expenses page actually filters — because every review round had correctly pointed out that nobody had ever run the app to check. The test compares how many charges appear with no filter against how many appear for a period the household definitely had no activity in.
Its first real run failed. Not on the filter — on a guard I had put in front of the comparison:
the demo household has no untracked subscription charges, so this test cannot prove the period filter does anything — seed one, or this is a false green
The test fixture was empty. Both counts were zero. Zero equals zero, so without that guard the test would have reported, cheerfully, that the filter worked — on the single question it existed to answer.
A test that can pass without touching the thing it tests is worse than no test, because it is now evidence. That guard cost four lines and it is the reason the eventual green means something. When the fixture was fixed, the test ran properly and the filter genuinely works — twenty-four passing, verified in a real browser against a real deployment, which is a sentence I could not have written a week ago.
And then, an hour later
I wrote all of this down as a lesson in the sprint’s closing notes. Grep the shape, not the line. Filed it, committed it, felt good about it.
Within the hour, the main branch went red.
A test about repeating tasks was failing by exactly one day. The cause is a date: it was August 31st. Ask JavaScript for “one month after August 31st” and it targets September 31st, which does not exist, so it rolls forward to October 1st. The application deliberately does not do that — it clamps to September 30th, which is what a person means. The test used the naive arithmetic; the code used the correct arithmetic; they disagreed by twenty-four hours.
Two days earlier, someone had found and fixed that exact bug in that exact file — in one of the five places it appeared.
It was not a regression. It was a bomb with a calendar for a fuse: harmless on the 30th, harmless on September 1st, and unmissable on the 31st. All five sites now derive their expected date from the application’s own arithmetic, so the test can no longer disagree with the code about what a month is.
I had written the lesson down an hour before the codebase demonstrated it to me. I do not have a tidy conclusion about that. It is just the most honest thing that happened this sprint.
Sprint 47, by the numbers: 63 PRs merged (#1777–#1869), a count taken at 03:44Z on the 31st itself, because a sprint closeout cannot fully count itself — two of those PRs are this report and the date-bomb fix it describes, both merged inside the window they’re reporting on. Three production releases, zero database migrations, three new entries in the regression catalogue. The expenses panel got the four fixes I asked for while using it on my own household. The public roadmap came off the help site. Continuous integration learned to run against a pull request’s own preview, to refuse a production database, and to say something when it goes quiet rather than only when it goes red.