Files
booth/persistent-memory.d/2026-09-23-blur-round-trip-and-the-migration-that-recreated-it.md
T

71 lines
3.7 KiB
Markdown

# 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 `.seen` shape) through a new stdlib-only `booth/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_self` resolved in `booth_items` from the same read as
`blurred`, replacing build_gallery's second `read_blurred`. That was a
two-reads-of-one-file seam (invariant 3).
- Built in a git worktree, because `scripts/booth` imports from the deployment
root LIVE: a half-built blur.py would have broken `booth blur` for 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 `.blurred` is lines only, read only while
`.blurred.json` is 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 `*..*` refused `a..b.png`, which the route accepted. There's
now one `check_rel` predicate for both, which also refuses an empty rel.
- kimi: `booth blur` without its package printed a bare traceback. It now
fails closed with exit 3, like `link`.
- 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 `.seen` shape); unreadable reads as
revealed (blur is cosmetic, the `.seen` posture).
## 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.toml` is 25/25.
- One row was vacuous on its first run (`set() or X` is `X`).
- Two open-flag rows went vacuous once `_load` lstat-checked for a regular
file first. They're now proved by direct `_read_capped` tests, because they
still close the lstat-to-open race.