bug-postmortem

  1. Rejected was not final

    A closed draft becomes published after all, exposing one missing transition in the blog registry's state model.

  2. The lock arrived late

    Seventeen blog-registry commits expose the small race between opening a draft and publishing its concurrency lock.

  3. One snake, two renderers

    The live board and canvas share image learn one shared rule for drawing a connected snake without merging a tight coil into a blob.

  4. The fourth status line

    A stale three-tool test turned pnpm support into a devcontainer startup failure, and the repair had to model the new update path properly.

  5. Silence was two different bugs

    One missing leaderboard entry exposed both silent server rejections and a client race that could erase a finished game without making a request.

  6. A directory is not a file type

    Narrowing CI's blog-only skip to blog/** looked complete. A review comment on the post documenting that very fix found the gap: a non-Markdown file placed under blog/ would skip every real check and ship anyway.

  7. The wrong duel to copy

    AI-duel let a fabricated score reach the public leaderboard for a game that was never played. The obvious model to copy was PvP, the other duel mode. The one that actually fit was Daily Challenge instead.

  8. A 404 is not an answer

    Skipping CI for blog-only PRs looked safe once branch protection came back 404. It wasn't: a separate ruleset required the same four checks by name, and the fix for that needed its own fix once a paths-filter default turned out to match everything.

  9. Three gaps, one bug

    Unifying daily and AI-duel score submission surfaced three suspected asymmetries. Two turned out to be features AI-duel simply has no use for, and the third was an error message with no reason to exist.

  10. Eight strings, one function

    A five-level nested ternary picked the caption for how a PvP game ended. The only way to exercise all eight of its outcomes was rendering the whole page.