Two Silent Traps in the Dev Environment

Four times in one day I reread the code because "the fix isn't taking." The culprit wasn't the code but the .next directory shared by dev and build, and a two-layer cache that kept serving old responses after I fixed the local DB. A trap the user never once saw

This is the last of the four days spent redrawing the legislative-activity tracker for desktop. Dev runs on a Next.js dev server, a Go backend and local PostgreSQL, and screen verification is measured with headless Chrome. During that verification a fix failed to show up on screen four times in one day, and each time the code got reread.

What I set out to do

The events in this post are not something I asked for. On the last of the four redesign days from episode 38, the agent stepped into the same trap four times while verifying. All I said was “go ahead,” “let’s go with option A,” “go with number 1,” and I never once saw the trap directly.

The dev setup for the legislative-activity tracker is this. A Next.js dev server, a Go backend, local PostgreSQL. The dev preview domain is attached through a tunnel on the Mac mini, so I can see it on the iPad too. Verification is measured with headless Chrome. So “the screen isn’t changing” leads straight to a mismeasurement.

How I asked

What I asked for was applying review feedback and testing. The first report came at 10:49 in the morning. The frontend was spitting 500s, and the cause was an old dev process that survived rm -rf .next. The second at 11. During testing it threw away the measurements and measured again. The port was LISTENing, so it looked “running.”

The third at 2:57 in the afternoon. “Hit the stale CSS cache again. Third time this session.” The fourth at 11:31 at night. “The globals.css change isn’t being reflected. 4th time this session. The item waiting to be documented is getting more and more certain.”

Where it broke

Trap 1. .next is shared by dev and build. The symptoms split two ways. If you run npm run build while dev is alive, build wipes dev’s route manifest. The dev process doesn’t die and the port stays open, but every request is a 500. There are cases where pkill doesn’t kill it, so you have to find it by port, force-kill it, delete .next, and restart.

The second symptom happens even without a build. Dev’s HMR misses a globals.css change entirely. Not just variables; the selector rules themselves are absent from the compiled output. I added one condition to an :active rule, and the browser’s CSS still had the old rule as it was. The source is fine, so you end up rereading the code wondering “is my code wrong?”

Trap 2. Fix the local DB and the API still gives the old response. It stepped into this one at 10:22 at night. To see the side-by-side comparison of current and amended text, it put seed data into the local DB, and the API stayed the same. It misdiagnosed it as “did the seed not go in?” The backend had a per-handler in-memory cache. 10 minutes for legislators, 5 minutes for bills. The cache was written down in the policy document, but the fact that it blocks screen verification during development was nowhere. That day it got past it by seeding a different bill that had not yet been requested even once.

How I fixed it

The next day I wrote two sections in the environment document. From here on is the real content of this post. Over 7 rounds the code review bot knocked down the document’s assertions one by one.

The assertion “5 minutes” was wrong to begin with. It differs per handler. “Seed a different record and it works” was only half right. The entity axis of the cache key decides. The key of the bill cache is the legislator, so seeding a different bill of the same legislator still hits. “Wait and it will always clear” did not hold on the screen. The SSR fetch had revalidate set, so the cache is two layers. I measured it. I planted value A in a cold bill, changed it to B, and at the point the backend TTL expired, the backend said B and the frontend said A. On re-request, B. Next’s data cache works as stale-while-revalidate.

It also came out that the “BE TTL” column in the policy document was actually the browser max-age. When I checked every route, of 17 routes the rule held for 7 and 10 deviated. It wasn’t a rule with deviations; there was no rule in the first place. The document deleted the causal narrative it had made up on its own. The commit message says “fourth overclaim in the same paragraph.”

There is one more side twist. The grep snippet I put in the document as the way to detect trap 1 created misdiagnoses instead. Dev’s CSS isn’t minified, so a rule spans multiple lines, but grep works line by line, so it returns 0 hits even when the rule is there. I caught it while actually running the snippet with the test skill. Grep only one unique fragment, and look at multi-line with Python.

Lessons