71 lines
3.7 KiB
Markdown
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.
|