3.7 KiB
The blur round-trip, and the migration that recreated the bug it fixed
2026-09-23 → 2026-09-24. Operator: "fix the blur." Commits 4cfbce5,
c1f5543 (merged 6880ab3), 8a78a9b.
The defect
design-dev's r2b bug-hunt found the /blur route stripping f, so the form for
" a.png" blurred "a.png". The route was only half of it: .blurred was one
stripped rel per line, so NO writer could store a rel with edge whitespace or a
newline. There were no live victims (6 legacy files, 42 rels, none with edge
whitespace; 0 live filenames with edge whitespace), so it was latent.
Round 1 (4cfbce5)
- JSON array (the
.seenshape) through a new stdlib-onlybooth/blur.py, so the CLI and the service share one reader and one writer. The CLI had its own grep/printf line writer, and after the format change it would have appended a line to a JSON array. Item.blurred_selfresolved inbooth_itemsfrom the same read asblurred, replacing build_gallery's secondread_blurred. That was a two-reads-of-one-file seam (invariant 3).- Built in a git worktree, because
scripts/boothimports from the deployment root LIVE: a half-built blur.py would have brokenbooth blurfor every session mid-TDD.
Round 2: heid bug-hunt (hulda, regin, kimi; groa timed out) → c1f5543
- 3/3: the migration recreated the bug. JSON went into the OLD file name
and the reader sniffed the format. A legacy file whose one line is an item
named
["a.png"]parses as JSON and blurs the neighbour. The docstring claimed that case was handled, and it wasn't. Fix: a NEW name,.blurred.json. The legacy.blurredis lines only, read only while.blurred.jsonis absent, and retired by the first write. - 2/3 + one: a planted directory 500'd the write path; the read path was hardened and the writer was not. Fix: the writer is judged by its reader (a postcondition), with BlurUnwritable answered as a 409.
- hulda (execution-verified): a lone surrogate
"\ud800"in planted JSON made every later write raise UnicodeEncodeError. Now dropped on read. - 2/3: the writer had no size cap, and the reader reads an oversized file as EMPTY. The writer now refuses first.
- 2/3: the CLI's
*..*refuseda..b.png, which the route accepted. There's now onecheck_relpredicate for both, which also refuses an empty rel. - kimi:
booth blurwithout its package printed a bare traceback. It now fails closed with exit 3, likelink. - Declined: the Item positional-constructor break (booth_items is the only
constructor, INV-1); the fdopen fd leak and the short read (not
constructible on a local fs, the
.seenshape); unreadable reads as revealed (blur is cosmetic, the.seenposture).
Round 3: groa's late retry → 8a78a9b
Its four bugs were the same four, already fixed. Its 0600 note ("a cross-uid
reader sees nothing and replaces it") exposed the real gap: set_blurred
built on read_blurred, the renderer's LENIENT reader, so an unreadable,
oversized or malformed file became an empty set and was overwritten. That is
the .marks.json wipe of 2026-09-21
(2026-09-21-tolerant-writer-over-tolerant-reader), repeated in a new module
and live for one night. Fix: _load is one parse with two postures (strict
for the writer, lenient for the renderer). It refuses only for a REGULAR file
it cannot read, since a link, a directory or a FIFO holds no set to lose. The
file is 0644 again.
Mutation notes
blur_storage.tomlis 25/25.- One row was vacuous on its first run (
set() or XisX). - Two open-flag rows went vacuous once
_loadlstat-checked for a regular file first. They're now proved by direct_read_cappedtests, because they still close the lstat-to-open race.