RETRO.md
Ref: Size: 7.1 KiB History
# Retro One line per push to `main`: what slowed the work down. Enforced by `.githooks/pre-push` — a push that adds no line here is refused. ## 2026-08-22 - Mutation testing: two full sweeps were spent before noticing the workdir filesystem, not the tool, was producing the numbers — ext4 reported 207 timed-out mutants and a flattering 100% efficacy where tmpfs reported 199 killed and an honest 93%. - The diff-scoped mutation check was wired without asking what an empty diff means. Pushing main answered it: origin/main became HEAD, gremlins read a diff with no entries as "everything changed", and the whole-tree run it started was killed by the one mutant that signals its own process group. - Wire rename: `make ci` failed twice on nothing wrong. A fresh worktree has an empty web/dist, so the web package misses its coverage floor until `make web` runs; and proto-check diffs generated code against the COMMIT, so it always fails on an uncommitted .proto change. Neither gate says so. - Wire-rename followups: proving "a v0.0.7 agent still talks to this server" meant rebuilding the old descriptor set from the tag by hand. The tree keeps every version of the .proto and no artifact of what a release actually put on the wire, so the compatibility claim had nowhere to be checked against. - Comment audit: six agents worked in parallel worktrees forked from the same commit, and by the time they finished, main had moved a rename underneath them. The merge was clean and would have silently reverted it — nothing in git distinguishes "did not touch this" from "restored the old name". Six deletions were abandoned because their mutation evidence was gathered against a tree that no longer existed. Proving the rest was comments-only needed a purpose-built token-stream comparator; go/printer reflows, so a diff that round-trips through it reads as changed when nothing was. - Volumes: the plan specified a schema FK (`volumes.host_id → hosts`) that broke host removal, and a 120s smoke cleanup window against a 5-minute reap grace; both were only caught by review against the live contracts, not by the task tests. ## 2026-08-23 - Loud misconfiguration: the survey of both call sites was done in a worktree still parked on the previous main, so the two tests the work was told not to duplicate looked like they did not exist and were nearly rewritten. A worktree's branch is not the branch you are about to fork from, and nothing says so. `make shape-check` also diffs the generated graph against the COMMIT, so adding one import fails the gate until the regenerated docs are committed — the same trap proto-check sets, in a second place. - Constant sweep: the third time this month `make ci` failed on a package the branch never touched. internal/server/web's floor is baselined against a built dist and a fresh worktree has none, so the gate reports a coverage regression where the real fact is "the UI was not compiled". It is written down twice above and was still rediscovered from scratch; a RETRO entry is not a gate. Filed. - Concurrency observers: the same stale-fork tax, one lane later. The worktree forked from a main five commits behind, so the first make ci failed on a coverage floor in a package this branch never touched, and the rebase landed on a wire rename mid-branch. Then a plain `git checkout <file>` to undo a mutation silently ate the uncommitted comment deletions sitting in the same file — mutation-test rounds and unstaged edits do not share a working tree. Stage before mutating, and restore with an explicit --source=HEAD. - Wrong comments: verifying cost more than fixing. Every line number in the filed issue was stale — the audit branch had already moved the files, and two of the eleven were fixed in passing there — so each site had to be found by content and then re-proved against the tree rather than trusted. Worse, the callers grep to disprove "the snapshot path uses FreezeCAs" printed `no matches found` because zsh expanded --include=*.go, not because there were no callers. A negative that arrives as a shell error reads exactly like a negative that arrives as an answer. Quote the glob, and treat an empty result as a claim to check rather than a claim to make. - Factory fixes: the merge_how proof ran the merger by hand and skipped the handler that strips merge_how from a part before the cross-source merge, so the fix that passed in the guest failed the first boot gate. Two more gate runs died at placement: a gate that dies does not reap its smoke VM, and the leftover plus a live test VM filled the devhost. Prove at the layer the code runs through, and check the host is empty before a run. - Volumes release: the branch's own refactor broke the release. Ordering moved to internal/version so the agent could reach it, and the ship script's tag probe kept compiling against internal/server/release.Less — a symbol nothing in the build graph references, so vet, lint and the boot gate all passed and stage 1 of the stg ship found it instead. Scripts that compile Go inline are outside every check the tree runs on itself. The rebase cost the same way twice over: HardDeleteVM had gained a hostID argument on main, and a fake API the new cloud-init test replaced wholesale stopped registering its VM, so the first ci after a clean rebase failed in two places that had nothing to do with volumes. - Site image: the ship stopped at stage 3 because nginx:alpine still carried seven HIGH util-linux advisories that Alpine had already fixed in its own package repo, and the image gate is right to refuse them. The base tag lags the distro, so a build is only as current as whenever upstream last rebuilt; taking the upgrade at build time is the difference between shipping and waiting on someone else's release cadence. - Backup image: the same base-tag lag as the site image, found one stage later and only on prod. BACKUPS=1 is a prod-only setting, so the new BACKUP_IMAGE requirement had never been exercised — stg proved nine stages and still could not have caught this. A plane-specific config gate is only proven on the plane that sets it. - Backup pull secret: baking sqlite in moved the job's image from Docker Hub to the plane's own registry, and the manifest kept the pull spec it had when the image was public. Nothing failed at apply time — a CronJob validates fine with no credential — so the break would have surfaced at 03:20 as an ImagePullBackOff nobody was watching, which is the exact failure the change set out to end. A workload that changes registries changes its auth, and only running the job proves it. - Scanner blindness: the flood of "Version matching error" lines during the v0.0.8 ship read as cosmetic noise and was the finding. trivy 0.58 could not parse the toolchain's go1.27.0-X:nodwarf5 stamp, so it matched no Go module against any advisory and said so once per module rather than once per image — loud enough to be ignored, quiet enough to look like a warning. A scanner that cannot read a version passes; only reading the summary and noticing the gobinary targets were missing would have caught it.