docs(contract): r3 compare — two picked items side by side, linked stepping, synced pan, flag the winner
Restores the contract as it stood at 1593ea2 (proposed a8428dc, booth-dev's seam pass folded ebd7729/33d9175/05ad6c4, heid's contract panel folded 1593ea2). Those commits lived only in a work clone under /tmp, which the 2026-09-24 reboot wiped; the text is unchanged.
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
---
|
||||
contract_version: "0.1"
|
||||
status: "PROPOSED 2026-09-24 by design-dev. The operator ruled compare into this arc on 2026-09-23 (the `flow` mark: compare `this_arc`, as C 'The Bench' made a view toggle). He ruled its two open questions on 2026-09-24, in design-dev's session: pairs are PICKED, never detected; the verdict is a FLAG on the winner, with no A/same/B record. booth-dev agreed with both beforehand (thread 01M3952NCDRRJX5XDFSPMSP5HJ), and asked that the URL be keyed by rel. It reuses r2c's stage machinery rather than growing a second copy."
|
||||
module: "GET /b/{name}/compare + templates/compare.html (two stages, one judgment each), with the stage machinery shared with view.html"
|
||||
purpose: "Put two items of a booth side by side in two equal stages (and, for pictures of the same size in 1:1, at the same crop), so the operator can judge which is better and flag the winner, then step to the next pair. This is the job the ladders and bakeoffs already run by eye across two tabs. sindra-bakeoff is the proving case: m against r, the same scene and seed, 16 pairs. It is laid out as two parallel runs in sorted order, so a linked step walks it pair by pair with no pairing rule."
|
||||
depends_on:
|
||||
- "items.py: `booth_items`, `review_chain` (the media ring, in item order), `find_item`, `REVIEW_KINDS`, `Item.{rel,url,kind,ordinal,caption,blurred,thumb}`."
|
||||
- "app.py: `resolve_booth`, `record_view`, `record_seen` (never raises), `marks_for` / `flagged_targets`, `_mark_redirect` (the JS-off landing, gains `back=compare`), `_mark_done` (the 204 path, unchanged)."
|
||||
- "view.html (R2 C6 + r2c): the review this is entered from; its stage script (Fit/1:1 as `stage-one` on <html>, drag-pan) is the machinery this unit shares."
|
||||
- "base.html: the head script that sets `stage-one` from `localStorage['booth.fit']` before any stage exists; the in-place client (`form[data-inplace]` -> 204 -> swap every `data-region`, then `booth:swapped`); Reveal all."
|
||||
language: "python (one route, one redirect branch) + jinja + css + javascript"
|
||||
complexity: "medium"
|
||||
estimated_loc: 420
|
||||
confidence: 0.75
|
||||
touches:
|
||||
- "booth/app.py (the compare route; `_mark_redirect` gains `back=compare`)"
|
||||
- "booth/templates/compare.html (new)"
|
||||
- "booth/templates/_stage_js.html (new: the stage machinery, moved out of view.html and shared)"
|
||||
- "booth/templates/view.html (includes _stage_js.html; gains the Compare entry and its C key)"
|
||||
- "booth/templates/base.html (compare layout CSS)"
|
||||
- "tests/test_compare.py, tests/test_compare_browser.py (new); tests/mutations/r3.toml (new)"
|
||||
assumptions:
|
||||
- "ONE VIEWER, as in R2: the linked toggle and the active side are per page load; the stage mode stays the r2c per-browser preference."
|
||||
- "No new storage and no new mark shape (the operator's ruling): the verdict is the existing flag, through the existing in-place POST."
|
||||
- "A pair is two items of the same booth's review ring. Comparing across booths is not this unit."
|
||||
---
|
||||
|
||||
# R3 — compare
|
||||
|
||||
## C1 — the route and the pair
|
||||
|
||||
- **`GET /b/{name}/compare?a=<rel>&b=<rel>`.** Both are booth-relative paths,
|
||||
exactly as `view?f=<rel>` takes one (U1 identity). **Never ordinals**: an
|
||||
ordinal is a position in the set as it is now (r2_flow, row 4). A file added
|
||||
mid-bakeoff shifts every later ordinal, so a bookmarked compare would open two
|
||||
different pictures and nothing would look wrong (booth-dev's seam note). The
|
||||
page PRINTS both ordinals.
|
||||
- **The rule for each side is a CONJUNCTION** (booth-dev's seam pass, S2). The
|
||||
review's rule alone is not enough, and neither is ring membership alone:
|
||||
1. the view route's resolve / containment / `is_file` check passes. That is
|
||||
what 404s a symlink pointing outside the booth, which `booth_items` DOES
|
||||
list, because it follows symlinks;
|
||||
2. AND the rel is in `review_chain(items)`. The review does not 404 a doc
|
||||
item (it renders it); compare does.
|
||||
Anything else is a **404**, never a 500: an embedded NUL raises ValueError
|
||||
and is a 404.
|
||||
- **A missing or empty param is a 404, not FastAPI's 422.** The review
|
||||
declares `f: str` and so answers 422 when `f` is absent. Compare declares
|
||||
`a: str = ""` and `b: str = ""`, 404s an empty one, and checks each with the
|
||||
same `isinstance(str)` the view branch gives `f`.
|
||||
- **Route order:** the compare route is registered BEFORE the catch-all
|
||||
`/b/{name}/{filepath:path}`, as view is. Accepted, and written down: a booth
|
||||
FILE literally named `compare` is unreachable at `/b/<name>/compare`. This
|
||||
is the same shadowing view, marks, asks and embed.json already cause.
|
||||
- `a == b` is allowed. It is pointless but harmless: the same picture twice.
|
||||
- **A look records both.** `record_view(booth)` once, and `record_seen` for `a`
|
||||
and then for `b`, below the 404s and gated on the records, as the view route
|
||||
gates it. Both calls never raise.
|
||||
- The response carries, per side: the rel, its quoted url, ordinal, kind,
|
||||
caption, blurred, flagged and thumb. It also carries the ring as a filmstrip
|
||||
in RING ORDER (the view route's `film`, one line in the route's docstring),
|
||||
the linked and per-side step targets (C3), the back link (the review of `a`,
|
||||
which is also where `Esc` goes), and `ord_width`.
|
||||
|
||||
## C2 — picking the two
|
||||
|
||||
- **From the review:** a `Compare` control in the review's top bar, and the key
|
||||
`C`, open `compare?a=<this item>&b=<the next item in the ring>`. With a ring
|
||||
of one item, `b` is the item itself.
|
||||
- **On the compare page, the filmstrip is the picker.**
|
||||
- Each frame is marked `A`, `B`, or nothing (both marks when `a == b`).
|
||||
- With JS, a click on a frame sets the ACTIVE side to that item and stays on
|
||||
the compare page. The active side defaults to B.
|
||||
- The active side wears the SVOS reticle (the one selection device). A click
|
||||
on either stage, or the key `X`, makes that side (or the other) active.
|
||||
- **The active side lives on a NON-region element**, the side's wrapper
|
||||
around its stage, and in the URL's `side` (C2), rewritten in place when it
|
||||
changes. An in-place save swaps regions (strip, labels, flags), and
|
||||
the in-place client carries only `revealed` and `is-closed` across a swap,
|
||||
so state kept on a region would be dropped. The strip's markers for the
|
||||
ACTIVE side are re-applied on `booth:swapped`. Strip clicks are delegated at
|
||||
the document, because the frames are replaced.
|
||||
- Without JS, every frame is a link that sets the active side from the URL
|
||||
(B by default): `compare?a=<a>&b=<frame>`.
|
||||
- **The view state rides in the URL too**, because every pick and step is a
|
||||
navigation, and state kept only in the page would reset on each one:
|
||||
- `side=a` makes A the active side (absent means B);
|
||||
- `link=0` unlinks the stepping (absent means linked).
|
||||
These are view state, not identity: an unknown value reads as the default,
|
||||
never as an error, and every server-built link carries the current values
|
||||
forward. The PAIR is still only the two rels. The route declares
|
||||
`side: str = ""` and `link: str = ""`, NOT an int or a Literal, which would
|
||||
bring S1's 422 back for `link=maybe`.
|
||||
- **Why B is the default active side:** A is where you came from, the anchor.
|
||||
B is what you are weighing it against, so a strip pick changes the
|
||||
comparison and not the anchor. `X` or a click on A makes A active.
|
||||
- The picked pair is ALWAYS in the URL. Every pick and every step is a
|
||||
navigation (a FULL page load) to a compare URL. The linked state and the
|
||||
active side survive it only because they ride the URL too (above); nothing
|
||||
else about the page is carried across a step, so the back button walks back through the pairs,
|
||||
and a reload shows the same pair.
|
||||
|
||||
## C3 — stepping
|
||||
|
||||
- **Linked (the default):** `←` and `→` move BOTH sides one place along the
|
||||
ring, keeping their distance: `(ia ± 1, ib ± 1)`, each modulo the ring
|
||||
length (the review's wrap). This walks a bakeoff's parallel runs: `#09 · #25`,
|
||||
then `#10 · #26`.
|
||||
- **Unlinked:** `←` and `→` move only the ACTIVE side.
|
||||
- The `Linked` toggle sits in the top bar, with the key `L`. Its state is the
|
||||
URL's `link` (C2): toggling it rewrites the current URL in place
|
||||
(`history.replaceState`) and the step links, so the next step keeps it. A
|
||||
fresh compare from the review starts linked. It is not stored anywhere
|
||||
else: a remembered unlinked state would surprise the next compare.
|
||||
- `Space` and `Shift+Space` act as `→` and `←`, with the review's guard: never
|
||||
from a focused control, and never while a player on either stage has focus.
|
||||
- **Every key on this page** (`←` `→` `Space` `A` `B` `X` `L` `Z` `C` `Esc`) is
|
||||
ignored while focus is in something editable, and whenever a modifier
|
||||
(Ctrl, Meta, Alt) is held: the review's `isEditable` rule, applied to all of
|
||||
them, not only to Space.
|
||||
- Without JS, the page renders plain links for "both back", "both forward",
|
||||
and each side's back and forward, with server-computed targets.
|
||||
|
||||
## C4 — the stages
|
||||
|
||||
- **Two stages side by side** when the viewport is wider than 900px. Each is
|
||||
half the body and labelled `A #09 <name>` / `B #25 <name>`. At 900px and
|
||||
below they STACK, A above B, each at most 45vh tall. The stack break is the
|
||||
review's.
|
||||
- **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
|
||||
1:1 picture that overflows. The picture cannot be dragged away. Video and
|
||||
audio play in their own stage.
|
||||
- **One mode for both:** the SAME `stage-one` class on `<html>` and the same
|
||||
`localStorage['booth.fit']`. Choosing 1:1 on the compare page is choosing it
|
||||
for the review, and back, because the mode is a per-browser preference
|
||||
(r2c S2). The `Fit | 1:1` toggle is in the top bar. The key `Z` switches
|
||||
it ON THE COMPARE PAGE ONLY, bound by compare.html and not by the shared
|
||||
include, so the review gains no key beyond `C` (INV-6). Provenance: r2c's
|
||||
out-of-scope lists "a key for the mode toggle" as its own line, not parked
|
||||
into r3. Compare takes it because comparing detail means switching modes
|
||||
often.
|
||||
- **Synced pan (parked into r3 by r2c): in 1:1, panning either stage pans the
|
||||
other to the SAME FRACTION of its scrollable range**, on each axis
|
||||
independently.
|
||||
- For two pictures of the same size, that is the same crop: the same pixels
|
||||
under the same point.
|
||||
- A side with nothing to scroll on an axis ignores that axis.
|
||||
- A scroll caused by the sync never re-triggers a sync, so there is no loop
|
||||
and no drift.
|
||||
- Scrollbars and wheel/trackpad scrolling sync the same way as drags, because
|
||||
the sync listens to `scroll`, not only to drags.
|
||||
- **One copy of the machinery, behind a stated interface** (S7: today's script
|
||||
is single-instance and ID-keyed, and `settle` is `place` + `pannable`). The
|
||||
include `_stage_js.html` defines two things and binds nothing by itself:
|
||||
- **`BoothMode.bind({toggle, fit, one, onChange})`: page level.** It owns the
|
||||
`stage-one` class, `setMode`, the pressed state, the storage writes and the
|
||||
cross-tab `storage` listener. It never raises. A page binds it ONLY when at
|
||||
least one of its stages is an image, which is the review's rule today; two
|
||||
videos get no toggle.
|
||||
- **`BoothStage.attach(stageEl, {img, onSettle})`: once per stage.** It owns
|
||||
`pannable()`, drag-to-pan (the 4px threshold, capture, scrollbar exclusion,
|
||||
`dragstart` prevention) and the stage's `can-pan` / `is-grabbing` classes.
|
||||
It calls `onSettle()` after its own settle, and it returns
|
||||
`{settle, pannable}`.
|
||||
- **view.html:** attaches its one stage with `onSettle = place`, so the
|
||||
arrows stay view's own code. It binds `BoothMode` exactly as today, and
|
||||
keeps EVERY id it has (`vstage`, `vimg`, `vtoggle`, `btn-fit`, `btn-one`,
|
||||
`vreveal`, `vmedia`, `vflag-btn`), because test_flow_browser queries them.
|
||||
Its reveal, keys, `centreFilm` and ResizeObserver stay in view.html. It
|
||||
has no floating arrows to add.
|
||||
- **compare.html:** attaches both stages and binds `BoothMode` once, with an
|
||||
`onChange` that settles both. Its per-side reveal buttons are its own code.
|
||||
It owns its OWN `ResizeObserver` over both stages, calling each stage's
|
||||
`settle`, because `pannable` changes on resize. `BoothStage.attach` does
|
||||
NOT own a ResizeObserver, so view.html's observer, and its mutation row,
|
||||
stay where they are.
|
||||
|
||||
## C5 — judging
|
||||
|
||||
- **Each side has its own flag control**, the existing flag form (`POST
|
||||
/b/{name}/flag`, `data-inplace`, 204 with JS). The verdict is "flag the
|
||||
winner", and flagging both is allowed. **What a flag records, stated plainly
|
||||
because it is the ruled trade-off:** a flag says "this one is good". Both
|
||||
flagged means both are good. Neither flagged means no call, OR a tie, and
|
||||
the flags cannot tell those apart. That is exactly why the A/same/B record
|
||||
was parked, not an oversight. Flagging the loser is the operator's
|
||||
prerogative; the page does not police it.
|
||||
- Keys: `A` toggles A's flag and `B` toggles B's. No `F` on this page,
|
||||
because which side it meant would be a guess. Each key LOOKS UP ITS BUTTON
|
||||
AGAIN at press time, because a save may have replaced it (the review's
|
||||
`vflag-btn` rule).
|
||||
- The flag controls, the filmstrip and each side's label are `data-region`s,
|
||||
so an in-place save refreshes them. The stages are never regions,
|
||||
because swapping one would restart a playing track (the review's rule).
|
||||
- **Region ids are unique on the page and keyed by SIDE** (S4): `flag-a`,
|
||||
`flag-b`, `label-a`, `label-b`, `film`. The swap keeps only the FIRST fresh
|
||||
node for each id and copies it over EVERY live node with that id. A shared
|
||||
`flag` id would therefore turn B's control into A's after any save, so
|
||||
that pressing B flagged A, and nothing would show it. The ids are NOT
|
||||
keyed by rel, which `a == b` would duplicate, and NOT prefixed `item-`,
|
||||
which the swap reads as a stale tile.
|
||||
- **Without JS, a flag lands back on the same compare page:** the form carries
|
||||
`back=compare`, `a`, `b`, `side` and `link`, and `_mark_redirect` builds
|
||||
`/b/<name>/compare?a=<quote(a, safe="/")>&b=<quote(b, safe="/")>` from them,
|
||||
appending `&side=a` ONLY when the form's value is exactly `a` and `&link=0`
|
||||
ONLY when it is exactly `0`, in that order. The view state is mapped from
|
||||
that closed set and never echoed. The URL has NO fragment (the flags sit beside the stages, so there is nothing to
|
||||
scroll to). It does that ONLY when both are strings in the ring, quoting each
|
||||
as the view branch does. It is built from the checked rels and
|
||||
never echoed from the form. Anything else takes the no-`back` landing. Every
|
||||
other `back` value is byte-identical to today (R2 INV-4).
|
||||
- Notes and the booth's open questions stay on the review. Compare carries only
|
||||
the flag, plus a `review A` / `review B` link on each side to the item's full
|
||||
review.
|
||||
|
||||
## C6 — blur and captions
|
||||
|
||||
- **Blur honesty per side:** a blurred item renders blurred, with its own
|
||||
reveal button over its stage (the review's pattern: JS-only, `hidden` until
|
||||
bound, never inside the scrolled content). Reveal all reveals both.
|
||||
- The blur CSS is scoped to `.review` (base.html: `.review .vstage.is-blurred
|
||||
img`, the `.revealed` and `is-img` rules, and Reveal all's `.reveal-all
|
||||
.review .vstage.is-blurred …`). The compare root is therefore
|
||||
`class="viewer review compare"`, and `.compare` overrides the review's
|
||||
4-row grid and the 360px rail column. The blur rules are not re-scoped:
|
||||
they and their r2b mutation rows stay as they are.
|
||||
- Each stage carries `vstage` and `is-img` (for a picture) and
|
||||
`is-blurred`, exactly as the review's does, so the 1:1 and blur rules
|
||||
apply unchanged.
|
||||
- Reveal all hides the review's stage reveal by ID
|
||||
(`.reveal-all #vreveal`). Compare's per-side reveals use a class,
|
||||
`cmp-reveal`, and base.html gains `.reveal-all .cmp-reveal{display:none}`.
|
||||
- compare.html carries `{% block html_attrs %} data-booth="{{ name }}"`,
|
||||
because without it Reveal all's script and the head script's reveal
|
||||
restore both bail (S6, r2b's mutation row for the review).
|
||||
- **Each side's caption** shows under its stage, clamped to 20vh with its own
|
||||
scroll, the same element style as the review's `.vcap`.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **INV-1 — rel identity.** The pair is two rels, in the URL, always. Nothing
|
||||
about the pair is stored, and no ordinal ever addresses an item.
|
||||
- **INV-2 — ring only.** Both sides are media in the review ring. Every
|
||||
server-computed link (the steps, the filmstrip, the flag landing) stays inside
|
||||
it.
|
||||
- **INV-3 — no new storage and no new mark.** The judgment is the existing flag,
|
||||
through the existing route and the existing in-place path.
|
||||
- **INV-4 — JS-off parity.** Without JS (and so without the head script that
|
||||
would apply a stored 1:1, which is itself a script): two Fit stages. Both
|
||||
sides' flag forms are present, so choosing which side to flag needs no
|
||||
picker; per-side and linked step
|
||||
links; filmstrip links that set B; flag forms that land back on the same pair.
|
||||
Nothing judgment-bearing hides behind a script.
|
||||
- **INV-5 — one record of the stage mode**, shared with the review:
|
||||
`stage-one` on `<html>`. Storage never raises.
|
||||
- **INV-6 — the review is unchanged in behaviour.** It gains a Compare control and
|
||||
a `C` key. The stage refactor changes no r2c assertion.
|
||||
|
||||
## TESTS
|
||||
|
||||
Server (`tests/test_compare.py`):
|
||||
- `compare_renders_the_pair` [tracer]: a booth of four images; `compare?a=<#1>&b=<#3>` → 200; both names and both ordinals are printed; the filmstrip marks #1 A and #3 B.
|
||||
- `a_bad_side_is_a_404`: a missing `a` or `b`, `..` traversal, a NUL, a dotfile, a doc item, a non-item file → 404 each, never 500.
|
||||
- `a_look_records_both_seen`: after a compare GET, `.seen` holds both rels; a 404 records nothing.
|
||||
- `linked_steps_keep_the_distance_and_wrap`: THE FIXTURE PUTS A DOC BETWEEN THE MEDIA (`03-notes.md`), so an ordinal is not a ring position. In a ring of 6 media with a at ring position 2 and b at ring position 5, "both forward" targets ring positions (3, 6), then (4, 1), wrapped; "both back" from (1, 4) targets (6, 3). The assertions name rels, never ordinals.
|
||||
- `the_urls_are_keyed_by_rel`: every step and filmstrip link carries `a=`/`b=` rels, url-quoted; no link carries an ordinal parameter.
|
||||
- `a_flag_without_js_lands_on_the_same_pair`: `POST /flag` with `back=compare&a=..&b=..` → 303 to exactly `/b/<name>/compare?a=..&b=..`; with `side=a&link=0` added → exactly `…&side=a&link=0`; with `side=A` or `link=00` → neither appended; with a rel not in the ring → the no-back landing; `Accept: application/json` → 204, unchanged.
|
||||
- `every_other_landing_is_byte_identical`: the existing `back=view` / `back=marks` / no-back redirects are unchanged (R2 INV-4).
|
||||
- `the_review_offers_compare_with_the_next_item`: with the doc fixture, the review of a media item links `compare?a=<it>&b=<the next media item in the ring>`, skipping the doc; the last media item links to the first.
|
||||
- `view_state_rides_the_links`: with `side=a&link=0`, every step and strip link carries both; an unknown `side=z` or `link=maybe` renders as B-active and linked, never an error.
|
||||
- `no_data_region_repeats`: on a compare page, including `a == b`, every `data-region` value is unique, and the side regions are `flag-a`, `flag-b`, `label-a` and `label-b`.
|
||||
- `a_missing_param_is_404_not_422`: `compare?a=<x>` without `b` → 404.
|
||||
- `an_outside_symlink_in_the_ring_is_404`: a booth symlink pointing outside the booth is in review_chain, and compare with it as either side → 404.
|
||||
- `compare_carries_data_booth`: the page's `<html>` carries `data-booth`.
|
||||
|
||||
Browser (`tests/test_compare_browser.py`):
|
||||
- `two_stages_side_by_side_wide_and_stacked_narrow` [tracer]: at 1440 both stages sit in one row; at 390 A sits above B and each is at most 45vh.
|
||||
- `one_mode_for_both_and_for_the_review`: `Z` switches both stages to 1:1; `localStorage['booth.fit']` is `one`; the review then opens in 1:1.
|
||||
- `synced_pan_lands_on_the_same_crop`: two equal-size pictures larger than the stage in 1:1. A drag on A of (+80, +60) scrolls both by (−80, −60). A scrollbar or wheel scroll on B moves A to the same fraction. Neither stage drifts after a second of idle.
|
||||
- `synced_pan_by_fraction_for_different_sizes`: a 2000px and a 3000px picture, one scrolled to its middle, puts the other at its middle; and one scrolled to 25% of its range puts the other at 25% of ITS range (not at the same pixel offset). This is the test that sees a fraction bug; the equal-size test above cannot, because equal overflow makes offsets and fractions coincide.
|
||||
- `a_flags_A_in_place_and_the_stages_survive`: press `A` → A's flag shows flagged with no navigation; the stage elements are the same nodes (a stage was not swapped).
|
||||
- `linked_arrow_walks_a_bakeoff`: in a booth shaped like sindra-bakeoff (lanes m and r, 4 pairs), open m#1 vs r#1 and press `→` three times: each pair shares its scene and seed suffix.
|
||||
- `unlinked_moves_only_the_active_side_and_the_strip_picks_it`: `L`, then `→`, moves only B, and a SECOND `→` still moves only B (the unlinked state survived the navigation); a click on a strip frame sets the active side's item; `X` swaps the active side and the reticle follows it, and survives the next step.
|
||||
- `blur_is_honest_on_both_sides`: a blurred B's image has a COMPUTED filter containing `blur(`, not just a class. Its own reveal clears it. Reveal all clears both, and hides both `cmp-reveal` buttons.
|
||||
- `a_save_keeps_the_active_side`: make A active, flag B in place → A is still active (reticle, strip marker), and a strip click after the swap still sets A.
|
||||
- `without_js_every_judgment_and_step_still_works`: JS off — the pair renders, the step and strip links navigate, and a flag lands back on the same pair.
|
||||
- `the_review_still_behaves_exactly_as_r2c_says`: the r2c browser suite passes unchanged against the refactored view.html. This is a gate, not a new test.
|
||||
|
||||
## Assertions that change (declared before the code)
|
||||
|
||||
| test | today | after | why |
|
||||
|---|---|---|---|
|
||||
| no behavioural assertion | — | — | view.html's behaviour is unchanged (INV-6). The only additions are the Compare control and the `C` key, which no current test pins. |
|
||||
| tests/mutations/r2c.toml: 21 rows anchor in view.html's script; the 15 on the toggle, the storage listener and drag-pan | `file = view.html`, anchors in the inline script | `file = _stage_js.html`, anchors re-pointed to the parameterised code (e.g. `stage.scrollLeft` becomes the attached stage's name) | the code moved (C4); every re-pointed row must still FALSIFY |
|
||||
| tests/mutations/r2c.toml: the other 6 of those 21, the arrow placement, including the resize row ("S3 the arrows do not follow a resize") | view.html | unchanged: `place()` and view's ResizeObserver stay in view.html (C4) | — |
|
||||
| tests/mutations/r2b.toml, the row on Space from a focused button | view.html's keydown | unchanged: the keydown handler stays in view.html | — |
|
||||
| new: tests/mutations/r3.toml | — | rows for: the side-keyed region ids, the conjunction 404, `back=compare`'s ring check, the linked distance, the synced-pan loop guard, the `data-booth` attribute, the `cmp-reveal` Reveal-all rule | the r3 falsifiers |
|
||||
|
||||
**The gate for the refactor is the TABLE, not only the suite:**
|
||||
`scripts/mutation_check.py tests/mutations/r2c.toml` (and r2b.toml) with every
|
||||
row falsifying after the move. A green r2c browser suite proves the behaviour
|
||||
survived. The table proves the tests still bind to the code that moved
|
||||
(booth-dev, S8).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Detecting pairs from filenames (ruled out: 1 of 26 live booths pairs by a name rule, and none of sindra-h2h does).
|
||||
- An A-better / same / B-better record (ruled: parked). If it is ever ruled in, it is a new mark keyed by an ORDERED pair of rels, in booth-dev's storage, with the JSON sessions read stated (booth-dev's note).
|
||||
- A zoom between Fit and 1:1, wheel zoom, and pinch (parked into r3 by r2c; parked again here: compare works at Fit and 1:1, and a third level is its own unit if the operator asks for it).
|
||||
- Three or more panes, onion-skin or swipe overlays, and a difference view.
|
||||
- Synced playback of two videos or two tracks (each stage plays on its own).
|
||||
- A grid multi-select to start a compare from the lightbox (the review's `C` and the picker strip cover picking).
|
||||
- Comparing across booths.
|
||||
Reference in New Issue
Block a user