tests/mutations/r2_flow.toml: 18 falsifiers, each proved RED under its
change by scripts/mutation_check.py (18/18). Its first run found three
vacuous proofs, now resolved:
- landed_at's per-entry skip: the symlink-loop fixture stopped raising
once the clock moved to lstat. New fixture: a folder that lists but
cannot be searched.
- the Desk's bench URL guard: the test covered bookmarks only. A
hand-edited registry bench now rides with it.
- flagged_targets' `error is None`: defence in depth (hydration already
strips a damaged mark's target), so no single-guard row; named in the
table header instead.
The rail's flagged filter and the orphan-flag list read flagged_targets
rather than restating it; no reachable behaviour changes.
In-place client (base.html):
- Saves are serialized: POST, re-fetch and swap complete before the next
save starts, so an older snapshot can no longer land after a newer one.
- A form already queued or in flight ignores another submit; a
double-click writes one note.
- Dirty controls (drafts, unsent radio choices) and disclosures carry by
identity (form action + hidden ask/target/mark/f + name), not position.
- Any non-tile structural difference, or a page with no region to swap,
reloads instead of patching.
Server and templates:
- .seen is a JSON array read without following links or blocking,
regular files of at most 1 MiB only; malformed, nested-too-deep or
planted markers read as nothing seen.
- landed_at reads symlinks by lstat and skips one unreadable entry
instead of pinning the booth in "new".
- The Desk counts flags on current items only; orphan flags are listed
under the tray with an unmark form.
- Agent-written bench and bookmark URLs link only when http(s).
- Audio and video tiles carry a review link.
- A rel the filesystem cannot represent is a 404, not a 500.
- A non-finite Accept q-value fails to parse.
- The standalone marks page has regions and updates in place.
- The review's next arrow sits at the edge at phone width.
Contract amended for each, plus an accepted-risks section (unlocked
.seen read-modify-write, a planted .viewed symlink, Item.ordinal with
no default).
741 passed. Each new browser test was mutation-checked against its fix;
the serialization test forces the race with a held first refresh, since
localhost alone never lost it.
Code fixes:
- The narrow-screen fold was specified and never built (4/4). The tray and
notes are now closed <details> in the aside; above 1000px CSS alone
(::details-content) shows them and hides the summary. There is no
script. Browser-tested at 390 and 1400, JS on and off.
- The lightbox gated on parsed board rows, not page identity (3/4). It now
uses is_board, the lesson the bench panel already carried.
- wants_json returned True at the first good entry, so a malformed later
entry was never read (3/4). It now parses every entry first; any error
is False.
- One flag predicate, flagged_targets. It serves the Desk count, the tray,
the filmstrip, the tape and the review button. An unreadable flag entry
counts nowhere.
- The header's open count and lifetime line, and the no-set marks panel,
are now regions (they were stale after an in-place answer).
- Inline group headers render only when every group is one contiguous run.
Interleaved directories no longer reprint or misfile headers.
- A booth held unreadable has no open_since, even with a readable pick
beside the damage.
- The swap marks an absent region is-stale instead of leaving it looking
current. It carries disclosure state (except the sent form's). The
failure message is readable for 0.9 s before the reload.
Contract amended where the code was right and the text was not: the
wants_json and record_seen signatures, landed_at's three refinements, the
group position being ring-based, the end of the set offering every other
open pick, the Space-key player exception, and the fold mechanism.
New tests cover the parse order; a board with media; the header region; the
no-set panel; interleaved groups; mixed damage; the flag predicate; the
review recording .viewed; the fold at two widths with JS on and off; the
status message before the reload; a lost response after a landed write
(exactly one note); a stale absent region; stage node identity across a
swap; and F with a radio focused. The lost-response and stale tests turn
red under their mutations. 724 passed.
- The zoom route becomes the review for image, video AND audio: the native
player on the stage for sound and video, the Fit/1:1 toggle for pictures
only. The judgment rail, the tape and the filmstrip are each a data-region.
The stage never is, so a playing track survives an in-place save.
- The rail shows the whole-set number, K of M in the review ring and the
position in the group; then the caption, and the flag and notes, landing
back here (back=view). A pick targeting this item is answerable in place.
On the last item the end-of-set block lists what was seen, the flags, and
every other open question.
- The keys are ← → Space F N Esc. Every one is ignored in an editable field,
and Esc returns to the grid at the tile you were on.
- _marks.html gains picks_only/back_view, so a pick form has one renderer
wherever it sits.
- In-place swaps now carry an unsaved draft across. A half-typed note
survives a flag, except in the form that was just sent.
- The filmstrip keeps the current frame in view.
- C7: no emblem in the chrome, pinned.
Browser tests cover: F typed into the note stays a letter and does not
flag; F outside the note flags in place and the draft survives; Space
moves; Esc lands on the grid tile. 706 passed.
- On a gallery booth the marks panel moves into a sticky verdict aside
beside the set. The aside comes first in the document, so a narrow screen
stacks the question above the work; grid areas place it on the right when
wide. Nothing in an ordered collection moves. Boards are unchanged.
- The flag tray lists flagged items by tile number: the declared change
from the panel list's (created, id). The standalone marks page keeps the
list.
- Inline group headers are divs, never figure.item.
- Every mark-dependent element is a data-region: the verdict, each tile,
the rail's filter counts. There is also a server-rendered status line.
- The in-place script (base.html) POSTs with an explicit JSON Accept, then
on 204 swaps every region from a fresh GET. Live media and per-viewer view
state are carried across the swap, so there is no layout jolt and no
stopped track. It never re-POSTs: on failure it says so and reloads. Tile
controls re-bind after a swap, and the grid cursor survives it.
- The `n` key opens the tile's closed note disclosure before focusing it.
- test_embed_browser's keyboard-flag test expected a navigation, which is
the defect R2 removes. It is updated as declared in the contract, and
tightened: a window marker must survive, proving no reload.
Browser tests: flag in place, with no reload and no scroll jump, and the
tile, tray and rail count all updated; and a failed save that reloads
without re-POSTing. Two mutations turn them red (no carry, no rail region).
700 passed.
- list_booths gains open_since (parsed, never compared as text), flags,
landed_at (content only; a new, differently named clock, INV-5),
viewed_at, and a four-image preview that keeps blur.
- The index renders needs you / new since you looked / everything else,
always in that order. Needs you includes unreadable marks, so a damaged
judgment file cannot hide. Everything else keeps list_booths' order
rather than stating a second rule. An empty section renders nothing.
- The side column holds live benches (a damaged registry says so),
bookmarks from BOOTH_LINKS_BOARD with booth URLs left out (capped at 8),
and the pickup form.
- test_booth's kept-lane test is rewritten as the contract declared: kept
is a fact on each row, not a lane.
Two of the new tests were VACUOUS on their first draft, and mutation-
checking caught both. The clocks test used a future t0, so a hand-set
marker outranked every real write. The look-then-judge test followed the
flag's 303, and the resulting GET recorded a fresh look. Both are fixed
and now go red under their mutation.
- wants_json: true only for an exact `application/json` entry with q > 0.
Absent, empty, wildcard, application/*, near misses, q=0 and malformed
headers all fall through to the 303.
- The four mark routes share one exit, _mark_done: 204 with no body for the
in-place client, otherwise _mark_redirect unchanged.
- back=view lands on /b/<name>/view?f=<rel>#rail, only for a media item of
this booth. It is built from the resolved rel and never echoed. Anything
else takes the no-`back` landing.
- tests/golden/r2_mark_303.json: 108 responses recorded from the PRE-R2
code (6 route cases x back absent|marks x 9 non-JSON Accepts), replayed
byte for byte (INV-4). Two mutations (q>=0, substring match) turn it red.
- The contract now states the q=0 rule.
- Item.ordinal: the 1-based position in booth_items over the items that
render. It is appended, and set in the resolver. Tiles print it padded to
the whole set's width, and a filter never renumbers.
- review_chain: the item order filtered to media. It replaces image_chain as
the zoom route's ring, so a set of pictures and sound steps through both.
image_chain stays importable.
- .seen: which media items were looked at full size, written by the review
route under record_view's gate. It is rewritten whole: deduplicated, pruned
to live items, sorted. The temp file is created with O_EXCL and swapped in
with os.replace, so a planted symlink is replaced, never written through.
It never raises.
Nine new tests. The contiguity and symlink tests are mutation-checked.
669 passed.