fix(r3): judge each rel once per request — a side or review item that vanishes mid-request never 500s

booth-dev's race note after the merge: the compare route resolved each side
in _compare_side and again in _compare_ring, then ring.index(a) raised if the
file vanished (or was relinked outside the booth) between the two; the review
did the same through cring.index(f). The compare ring is now built once and
the sides are judged by membership of it. The review re-judges its item and
scans forward for the next comparable one (usually one step, no longer a
resolve of the whole ring per render); an item no longer comparable renders
the review without a Compare control, and C does nothing.

The contract records the once-per-request rule and that the phone-width wrap
covers doc.html's bar too. r3.toml: 59 rows, four re-anchored.
This commit is contained in:
vh
2026-09-24 15:59:43 -07:00
parent cf08ae3f33
commit 8633b1dded
5 changed files with 118 additions and 32 deletions
+11 -3
View File
@@ -62,6 +62,13 @@ assumptions:
stays in the review ring). EVERY compare link is built from it: the strip,
the steps, the review's Compare control and the `back=compare` landing. So
no navigation offers a pair that 404s, and a step walks over such an item.
- **Each rel is judged ONCE per request** (booth-dev, after the merge). The
route builds the compare ring once and judges both sides by membership of
it. Resolving a rel twice lets a file that vanishes between the two reach a
lookup that raises, a 500. The review re-judges its own item first and scans
forward for the next comparable one; when its item is no longer comparable,
the review renders WITHOUT a Compare control (and `C` does nothing), never a
500.
- The response carries, per side: the rel, its quoted url, ordinal, kind,
caption, blurred, flagged and thumb. It also carries the compare ring as a
filmstrip in RING ORDER (the view route's `film`, one line in the route's docstring),
@@ -142,9 +149,10 @@ assumptions:
gap, never a border that comes out of one side's width. Two stages of
different sizes would draw the same picture at two scales in Fit.
- **At phone width (600px and below) a top bar that cannot hold its controls
WRAPS** instead of scrolling the page sideways or crushing a control. This
applies to the review's bar too, which gains the Compare control (only its
glyph below 600px). The review's bar was already full: a fogged booth
WRAPS** instead of scrolling the page sideways or crushing a control. The
rule is on `.vbar`, so it applies to every bar of that class: compare's, the
review's (which gains the Compare control, only its glyph below 600px), and
doc.html's. The review's bar was already full: a fogged booth
overflowed it by 3px at 390px before r3.
- **Each stage is the r2c stage:** Fit fills (up or down, contain, never
cropped), or 1:1 at natural pixels with every pixel reachable. Drag pans a