fix(as-S5a): every control named, one h1 and a skip link, rings and hit areas

The markup and CSS half of the anti-slop interaction work. The in-place
client is untouched (that is S5b).
- Glyph-only controls carry a name: the withdraw ×s, downloads, open full
  page, the viewers' ✕, the board's pin, copy and remove, the bench's
  remove, the 1:1 toggle ("1:1, natural pixels"). Film-strip and tray
  frames carry the file's name as sr-only text instead of reading "01".
  A Desk row's wipe names its booth.
- Fields are named by aria-label, not by their placeholder.
- The inline ask's options are a radiogroup labelled by the prompt; a
  single-question fieldset gets an sr-only legend; a titled ask's title
  takes bk-ask-<id>-title (it duplicated the question's id).
- One h1 per page (sr-only on the Desk, review and compare), a skip link
  to <main id="main">, theme-color for light and dark.
- The review tape is one picture (role=img); its segments leave the tab
  order (the film strip holds the same links, named).
- Wipe now uses the Desk's delegated prompt, moved to base.html: it names
  the booth and asks the kept-booth question for a kept booth.
- Embed focus rings of its own; rings drawn inside clipping containers;
  the withdraw × at least 24px, 44px under a coarse pointer;
  touch-action:manipulation; strips contain their overscroll; a long
  slug wraps on a phone.
- A truncated why carries its full text in title; a countdown of 48h or
  more reads in days.
Two r2_flow.toml rows for the confirm helper now name base.html, where
the helper moved (anchors unchanged; the gate found them drifted).

Contract: as_antislop S5a. Falsifiers: antislop.toml 86/86 proved (S1-S6, S5a);
all 12 tables 366/366 proved on this tree.
This commit is contained in:
vh
2026-09-28 13:30:31 -07:00
parent ec807fe41b
commit d4f64fd7ec
16 changed files with 675 additions and 82 deletions
+268
View File
@@ -375,3 +375,271 @@ file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_the_hazard_stripe_is_a_pseudo_element"
old = """ .wipe-lg button::before,.board-del-sel:not(:disabled)::before{content:"";position:absolute;"""
new = """ .wipe-lg button::after,.board-del-sel:not(:disabled)::after{content:"";position:absolute;"""
# ---- S5a: accessibility plumbing (names, landmarks, focus, hit areas)
[[mutation]]
label = "S5a the withdraw-note × unnamed again"
file = "booth/templates/_marks.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """<button type="submit" class="mark-x" title="withdraw this note" aria-label="withdraw this note">×</button>"""
new = """<button type="submit" class="mark-x" title="withdraw this note">×</button>"""
[[mutation]]
label = "S5a a film frame named only by its number"
file = "booth/templates/view.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ <span class="sr-only">{{ x.name }}</span>{%- if x.kind == 'image' %}<img loading="lazy" decoding="async" src="{{ x.thumb or x.url }}" alt="">{% else %}<span class="film-kind">"""
new = """ {%- if x.kind == 'image' %}<img loading="lazy" decoding="async" src="{{ x.thumb or x.url }}" alt="">{% else %}<span class="film-kind">"""
[[mutation]]
label = "S5a a flagged-tray item named only by its number"
file = "booth/templates/_marks.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """<span class="sr-only">{{ it.name }}</span>{%- if it.kind"""
new = """{%- if it.kind"""
[[mutation]]
label = "S5a the 1:1 button read as digits"
file = "booth/templates/view.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ aria-label="1:1, natural pixels" title="natural pixels — drag to pan a large picture">"""
new = """ title="natural pixels — drag to pan a large picture">"""
[[mutation]]
label = "S5a the doc's open-full-page glyph unnamed"
file = "booth/templates/booth.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ title="open full page" aria-label="open {{ it.name }} full page">⤢</a>"""
new = """ title="open full page">⤢</a>"""
[[mutation]]
label = "S5a compare's close glyph unnamed"
file = "booth/templates/compare.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ title="back to the review of A (Esc)" aria-label="back to the review of A">✕</a>"""
new = """ title="back to the review of A (Esc)">✕</a>"""
[[mutation]]
label = "S5a the board's pin star unnamed"
file = "booth/templates/booth.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ aria-pressed="{{ 'true' if e.pinned else 'false' }}" aria-label="pin {{ e.desc }}""""
new = """ aria-pressed="{{ 'true' if e.pinned else 'false' }}""""
[[mutation]]
label = "S5a the board's remove × unnamed"
file = "booth/templates/booth.html"
test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name"
old = """ title="remove this link" aria-label="remove the link {{ e.desc }}""""
new = """ title="remove this link""""
[[mutation]]
label = "S5a a Desk wipe that does not say which booth"
file = "booth/templates/index.html"
test = "tests/test_antislop.py::test_desk_row_controls_name_their_booth"
old = """aria-label="wipe the booth {{ b.name }}""""
new = """aria-label="wipe booth""""
[[mutation]]
label = "S5a the note field named only by its placeholder"
file = "booth/templates/_marks.html"
test = "tests/test_antislop.py::test_fields_are_named"
old = """<textarea name="text" rows="2" aria-label="a note on this booth, for the session that posted it" placeholder"""
new = """<textarea name="text" rows="2" placeholder"""
[[mutation]]
label = "S5a the bench URL field unnamed"
file = "booth/templates/booth.html"
test = "tests/test_antislop.py::test_fields_are_named"
old = """<input type="url" name="url" aria-label="https://host:port/" placeholder"""
new = """<input type="url" name="url" placeholder"""
[[mutation]]
label = "S5a the embed's notes field unnamed"
file = "booth/templates/_ask_inline.html"
test = "tests/test_antislop.py::test_fields_are_named"
old = """ aria-label="{{ a.notes_label }}" placeholder="{{ a.notes_label }} (optional)">"""
new = """ placeholder="{{ a.notes_label }} (optional)">"""
[[mutation]]
label = "S5a the inline ask's options not a named group"
file = "booth/templates/_ask_inline.html"
test = "tests/test_antislop.py::test_radio_groups_are_named_and_ids_are_unique"
old = """<div class="bk-ask-opts" role="radiogroup" aria-labelledby="""
new = """<div class="bk-ask-opts" aria-labelledby="""
[[mutation]]
label = "S5a the group names a prompt id that is not there"
file = "booth/templates/_ask_inline.html"
test = "tests/test_antislop.py::test_radio_groups_are_named_and_ids_are_unique"
old = """<p class="bk-ask-prompt" id="bk-ask-{{ a.id }}{% if q.key %}-{{ q.key }}{% endif %}-prompt">"""
new = """<p class="bk-ask-prompt">"""
[[mutation]]
label = "S5a a titled ask's title reuses the question's id"
file = "booth/templates/_ask_inline.html"
test = "tests/test_antislop.py::test_radio_groups_are_named_and_ids_are_unique"
old = """<p class="bk-ask-title" id="bk-ask-{{ a.id }}-title">"""
new = """<p class="bk-ask-title" id="bk-ask-{{ a.id }}">"""
[[mutation]]
label = "S5a a single-question fieldset without a legend"
file = "booth/templates/_marks.html"
test = "tests/test_antislop.py::test_radio_groups_are_named_and_ids_are_unique"
old = """{% else %}<legend class="sr-only">{{ q.prompt }}</legend>{% endif %}"""
new = """{% endif %}"""
[[mutation]]
label = "S5a the Desk without an h1"
file = "booth/templates/index.html"
test = "tests/test_antislop.py::test_every_page_has_one_h1_and_a_skip_link"
old = """<h1 class="sr-only">Desk</h1>
"""
new = """"""
[[mutation]]
label = "S5a the review without an h1"
file = "booth/templates/view.html"
test = "tests/test_antislop.py::test_every_page_has_one_h1_and_a_skip_link"
old = """<h1 class="sr-only">{{ file }}</h1>
"""
new = """"""
[[mutation]]
label = "S5a compare without an h1"
file = "booth/templates/compare.html"
test = "tests/test_antislop.py::test_every_page_has_one_h1_and_a_skip_link"
old = """<h1 class="sr-only">compare {{ sides.a.rel }} and {{ sides.b.rel }}</h1>
"""
new = """"""
[[mutation]]
label = "S5a no skip link"
file = "booth/templates/base.html"
test = "tests/test_antislop.py::test_every_page_has_one_h1_and_a_skip_link"
old = """<a class="skip-link" href="#main">Skip to content</a>
"""
new = """"""
[[mutation]]
label = "S5a the skip link points at nothing"
file = "booth/templates/base.html"
test = "tests/test_antislop.py::test_every_page_has_one_h1_and_a_skip_link"
old = """<main id="main">"""
new = """<main>"""
[[mutation]]
label = "S5a no dark theme-color"
file = "booth/templates/base.html"
test = "tests/test_antislop.py::test_theme_color_for_both_schemes"
old = """<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#15191d">
"""
new = """"""
[[mutation]]
label = "S5a the tape is a list of links again"
file = "booth/templates/view.html"
test = "tests/test_antislop.py::test_the_tape_is_one_picture"
old = """<div class="tape" data-region="tape" role="img" aria-label="""
new = """<div class="tape" data-region="tape" aria-label="""
[[mutation]]
label = "S5a the tape's segments back in the tab order"
file = "booth/templates/view.html"
test = "tests/test_antislop.py::test_the_tape_is_one_picture"
old = """ title="{{ num(x.ordinal) }} {{ x.name }}" tabindex="-1" aria-hidden="true"></a>"""
new = """ title="{{ num(x.ordinal) }} {{ x.name }}"></a>"""
[[mutation]]
label = "S5a Wipe now back on an inline anonymous prompt"
file = "booth/templates/booth.html"
test = "tests/test_antislop.py::test_wipe_now_asks_by_name"
old = """ data-booth="{{ name }}" data-confirm="{{ 'wipe-kept' if kept else 'wipe' }}">"""
new = """ onsubmit="return confirm('Wipe this booth now?')">"""
[[mutation]]
label = "S5a a kept booth's Wipe now asks the ordinary question"
file = "booth/templates/booth.html"
test = "tests/test_antislop_browser.py::test_wipe_now_asks_by_name_in_the_browser"
old = """data-confirm="{{ 'wipe-kept' if kept else 'wipe' }}""""
new = """data-confirm="wipe""""
[[mutation]]
label = "S5a the shared prompt does not reach a booth page"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_wipe_now_asks_by_name_in_the_browser"
old = """ var form = ev.target.closest ? ev.target.closest('form[data-confirm]') : null;"""
new = """ var form = ev.target.closest ? ev.target.closest('.desk form[data-confirm]') : null;"""
[[mutation]]
label = "S5a hours never roll up to days"
file = "booth/app.py"
test = "tests/test_antislop.py::test_human_dur_rolls_up_to_days"
old = """ if h >= 48:"""
new = """ if h >= 4800:"""
[[mutation]]
label = "S5a the embed's submit button has no ring of its own"
file = "booth/static/embed.js"
test = "tests/test_antislop.py::test_embed_chrome_draws_its_own_focus_rings"
old = """ ".bk-ask-go:focus-visible{outline:2px solid var(--bk-accent);outline-offset:2px}",
"""
new = """"""
[[mutation]]
label = "S5a an embed option has no ring"
file = "booth/static/embed.js"
test = "tests/test_antislop.py::test_embed_chrome_draws_its_own_focus_rings"
old = """ ".bk-ask-opt:has(input:focus-visible){outline:2px solid var(--bk-accent);outline-offset:1px}",
"""
new = """"""
[[mutation]]
label = "S5a rings drawn outside a clipping container"
file = "booth/templates/base.html"
test = "tests/test_antislop.py::test_focus_rings_are_drawn_inside_clipping_containers"
old = """ .desk-panel a:focus-visible{outline-offset:-2px}"""
new = """ .desk-panel a:focus-visible{outline-offset:2px}"""
[[mutation]]
label = "S5a a truncated why without its full text"
file = "booth/templates/_provenance.html"
test = "tests/test_antislop.py::test_a_truncated_why_carries_its_full_text"
old = """<span class="prov-why" title="{{ m.why }}">"""
new = """<span class="prov-why">"""
[[mutation]]
label = "S5a the withdraw × at its old size on a desktop"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_withdraw_buttons_are_big_enough_to_hit"
old = """ .mark-x{min-width:24px;min-height:24px}"""
new = """ .mark-x{}"""
[[mutation]]
label = "S5a the withdraw × not enlarged under a finger"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_withdraw_buttons_are_big_enough_to_hit"
old = """ @media (pointer:coarse){.mark-x,.flagtoggle button"""
new = """ @media (pointer:coarse){.flagtoggle button"""
[[mutation]]
label = "S5a double-tap zoom delay back on controls"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_touch_and_scroll_behaviour"
old = """ a,button,summary,label,input,select,textarea{touch-action:manipulation}"""
new = """ summary,label,input,select,textarea{touch-action:manipulation}"""
[[mutation]]
label = "S5a the film strip hands its scroll to the page"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_touch_and_scroll_behaviour"
old = """ .film,.vcap,.doc-body,.cmp-strip{overscroll-behavior:contain}"""
new = """ .vcap,.doc-body,.cmp-strip{overscroll-behavior:contain}"""
[[mutation]]
label = "S5a a long slug runs off a phone"
file = "booth/templates/base.html"
test = "tests/test_antislop_browser.py::test_touch_and_scroll_behaviour"
old = """ @media (max-width:600px){.h1-slug{white-space:normal;overflow-wrap:anywhere}}"""
new = """ @media (max-width:600px){.h1-slug{}}"""
+2 -2
View File
@@ -187,7 +187,7 @@ new = '''
[[mutation]]
label = "the wipe dialog shows the agent-made name raw (bidi, newline)"
file = "booth/templates/index.html"
file = "booth/templates/base.html" # as S5a: the confirm helper moved here from index.html
test = "tests/test_flow_browser.py::test_the_wipe_dialog_shows_what_is_being_wiped_and_never_fails_open"
old = '''
if (!confirm(word(shown(form.getAttribute('data-booth') || '')))) ev.preventDefault();'''
@@ -196,7 +196,7 @@ new = '''
[[mutation]]
label = "an unknown data-confirm word submits with no prompt (fail open)"
file = "booth/templates/index.html"
file = "booth/templates/base.html" # as S5a: the confirm helper moved here from index.html
test = "tests/test_flow_browser.py::test_the_wipe_dialog_shows_what_is_being_wiped_and_never_fails_open"
old = '''
var word = WORDS[form.getAttribute('data-confirm')] || ASK;