memory: snapshot for context clear — waiting on design-dev's r3 contract

This commit is contained in:
vh
2026-09-24 08:27:31 -07:00
parent d5ead3f613
commit 47b39bca53
5 changed files with 216 additions and 101 deletions
@@ -0,0 +1,70 @@
# 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.
@@ -0,0 +1,30 @@
# The Desk's "Everything else" sorts by last UPDATE, not last activity
_2026-09-23. Commit `64f6488`. Operator: "how is this last activity first?",
then "Easier — last activity can just be last time the booth was updated, not
necessarily operator's last activity."_
## What was wrong
- The section sorted by `_newest_mtime`, which counts `.viewed`. Every GET of a
booth, marks or review page records a look.
- Two sessions' post-deploy GET sweeps (17:48 and 21:46, both 127.0.0.1)
recorded a look at 22 booths within half a second. The section collapsed
into reverse name order through the `(mtime, name)` reverse tie-break.
- The rows show "updated X ago" (`landed_at`), a different clock from the sort.
- The same sweeps emptied "new since you looked". The access log showed 3
booths never opened from a 10.0.10.x or 100.64.x device: `dfa-landing`,
`ldp-polish`, `pewpew-ui-brief`.
- **The operator declined repairing `.viewed` from the log** (the mesh IPs
can't be attributed with certainty), and declined gating views on
`Sec-Fetch-Dest`. Both were offered.
## The fix
- `rest.sort(key=(-landed_at, name))`, labelled "last updated first".
- `list_booths` keeps its order for its other readers, and `_newest_mtime`
still feeds lifetime.
- The r2_flow contract (§3, the ordering table, INV-5) and the ROADMAP ordering
row were amended.
- CLAUDE.md "Working in here" now says how to check live without recording a
look.
@@ -45,3 +45,14 @@ branch as its own commit.
⚠ **The methodological trap, and why the artefact must come from the full
suite:** the failure only reproduces there. A narrowed repro that passes proves
nothing and will be mistaken for a fix.
## Addendum 2026-09-24 — the offline fix, and what 0/24 can and cannot say
design-dev's suspect was Google Fonts stalling "networkidle". A stalled font
request reproduces the exact error (which shows sufficiency only). The fix
landed in `b92b002`: the test browser has no internet, with a positive control
per fixture. Traced runs: 0/15 (light) and 0/8 (heavy). No trace ever caught
the stalled request. Untraced after the fix: **0 reds in 24**, against a pre-fix
rate of 1 in 8 that is itself one red in eight runs (95% CI roughly 0.3–53%).
At a true rate of 1 in 20, 0/24 happens 29% of the time. So it's consistent
with the fix and not a confirmation of the cause.
@@ -0,0 +1,44 @@
# Thumbnails sized for the tile's width at 2x, not 512 on the long side
_2026-09-23. Operator on sindra-nude-final: "the images look blurry until
they're selected and blown up." Commits `c2b1454`, `c19d8c9`, merged
`1d31ab0`._
## The measurement
- The cap was 512px on the LONGEST side. Tiles are sized by WIDTH (`width:100%;
height:auto`).
- Chromium put desktop tiles at 321-361 CSS px (3 columns, 1440px viewports
and up), 324-472 at 2 columns, and up to 650 at 1 column (measured across a
360-2560 viewport sweep).
- A 704x1408 portrait got a 256-wide thumbnail: stretched 1.4x at 1x density
and 2.8x on a 2x screen.
- The live sizes are mostly 704x1408, 896x1216, 832x1216 and 1024x1024
portraits (368 images sampled).
## The rule
- `THUMB_WIDTH = 768` (2x the widest desktop tile) and `THUMB_HEIGHT_MAX =
4096`.
- An original that fits is served as-is only when it's also light (<=64 KB;
768-wide thumbnails average 39 KB) or animated.
- `tests/test_thumbs_browser.py` binds 768 to the rendered grid at 1440, 1920
and 2560.
- Cost across all 381 live images: 4.8 → 14.2 MB at 768; 1024 would have been
18.5 MB. Live rebuild: 14.5 MB, 369 webp and 12 originals, 9 s.
- **The operator kept 768 (2026-09-24).**
## heid bug-hunt (4/4, five seat-executed probes) → `c19d8c9`
- A cache hit must be a REGULAR file with its source's EXACT mtime. A planted
directory was served as the thumbnail, and `cp -p` pinned stale ones
forever.
- The cache dirs are made component by component without following links. A
`.thumbs` link put the cache outside the booth.
- The temp file is mkstemp. `<out>.<pid>.tmp` was plantable as a link: 600 B
became 316,400 B.
- Palette transparency survives. This one was INTRODUCED by `c2b1454`'s
fits-but-heavy branch.
- EXIF orientation is honoured.
- A 64 MP decode budget.
- The whole rule is in the cache name: `.768x4096q78v2.webp`.