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
+32 -8
View File
@@ -19,11 +19,9 @@ label = "C1 the conjunction loses the ring (a doc or a sidecar opens as a side)"
file = "booth/app.py"
test = "tests/test_compare.py::test_a_bad_side_is_a_404"
old = '''
if rel not in ring:
raise HTTPException(status_code=404, detail="no such item")
return rel'''
return [r for r in review_chain(items) if _in_booth(booth, r)]'''
new = '''
return rel'''
return [it.rel for it in items if _in_booth(booth, it.rel)]'''
[[mutation]]
label = "C1 a missing side is FastAPI's 422 (no default)"
@@ -48,12 +46,12 @@ test = "tests/test_compare.py::test_a_look_records_both_seen"
old = '''
booth = resolve_booth(name)
items = booth_items(booth)
a = _compare_side(booth, review_chain(items), a)'''
ring = _compare_ring(booth, items)'''
new = '''
booth = resolve_booth(name)
record_view(booth)
items = booth_items(booth)
a = _compare_side(booth, review_chain(items), a)'''
ring = _compare_ring(booth, items)'''
[[mutation]]
label = "C6 compare does not carry data-booth (Reveal all and its restore bail)"
@@ -114,8 +112,8 @@ new = ''' side_a = side not in ("", "b")'''
label = "C2 the review's Compare does not wrap (the last item compares with itself)"
file = "booth/app.py"
test = "tests/test_compare.py::test_the_review_offers_compare_with_the_next_item"
old = ''' f"&b={quote(cring[(cring.index(f) + 1) % len(cring)], safe='/')}"),'''
new = ''' f"&b={quote(cring[min(cring.index(f) + 1, len(cring) - 1)], safe='/')}"),'''
old = '''(ring[(pos + k) % len(ring)] for k in range(1, len(ring) + 1))'''
new = '''(ring[min(pos + k, len(ring) - 1)] for k in range(1, len(ring) + 1))'''
# ---- C5: the regions and the JS-off flag landing
@@ -543,6 +541,32 @@ old = '''
new = '''
return p && !/^(side|link)(=|$)/.test(p);'''
# ---- after the merge: booth-dev's race note (01M3AT7GKCPATJD5YW0PR3SRPT)
[[mutation]]
label = "C1 a side is judged twice (the ring rebuilt per side): a side that vanishes between is a 500"
file = "booth/app.py"
test = "tests/test_compare.py::test_a_side_that_vanishes_mid_request_never_500s"
old = '''
ring = _compare_ring(booth, items) # built ONCE; every rel judged once
a = _compare_side(ring, a)
b = _compare_side(ring, b)'''
new = '''
a = _compare_side(_compare_ring(booth, items), a)
b = _compare_side(_compare_ring(booth, items), b)
ring = _compare_ring(booth, items)'''
[[mutation]]
label = "C2 the review offers Compare for an item that vanished after its own check"
file = "booth/app.py"
test = "tests/test_compare.py::test_the_review_hides_compare_when_its_item_vanishes_mid_request"
old = '''
if _in_booth(booth, f):
partner'''
new = '''
if True:
partner'''
# Refuted, not rowed (bug hunt): "a right-click mid-drag ends the pan" — a
# second button pressed and released during a drag arrives as chorded
# `pointermove` events, never a `pointerup` (measured 3/3 in the test browser,