From 7143fae6c7742add0d1c39239f0c770cb3fe1979 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 28 Sep 2026 13:58:52 -0700 Subject: [PATCH] =?UTF-8?q?fix(as-S5a):=20fixup=20from=20booth-dev's=20gat?= =?UTF-8?q?e=20=E2=80=94=20release=20asks,=20fail-closed=20words,=20ids=20?= =?UTF-8?q?that=20cannot=20collide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From booth-dev's hulda bug-hunt with heid's second voice (BRINGA, thread 01M3MVGQ7QSCCK8WT59TQ4J469): - The booth page's "kept — release" asks by name, as the Desk's does. - WORDS has no prototype: data-confirm="__proto__" or "constructor" is an unknown word, and asks, instead of throwing before preventDefault. - The confirm helper moved into : its capture listener exists before any form, so a click during load is asked too (the inline confirm() it replaced had that property). - shown() also marks U+2028/U+2029 and the zero-width characters. - Derived ids take ':' (bk-ask--:prompt, bk-ask-:title), which no id or key can contain; '-prompt' and '-title' collided with valid keys. booth-dev's chip test now looks its fragment up by [id=...]. - human_dur says "—" for a value that is not finite, instead of raising. - The tile's copy of a note drops its id (booth-dev: mark- is the panel's article). - Four guards that asserted source patterns now also hold on computed effects: embed rings, rings inside clipping containers, the withdraw × on both axes, and question-level notes fields. Contract: as_antislop S5a (fixup). Falsifiers: antislop.toml 102/102 with r2_flow.toml 24/24 proved; the full gate follows. --- booth/app.py | 2 + booth/templates/_ask_inline.html | 6 +- booth/templates/base.html | 86 ++++++++-------- booth/templates/booth.html | 7 +- docs/contracts/as_antislop.contract.md | 10 ++ tests/mutations/antislop.toml | 131 ++++++++++++++++++++++++- tests/test_antislop.py | 58 ++++++++++- tests/test_antislop_browser.py | 119 +++++++++++++++++++++- tests/test_embed_browser.py | 4 +- 9 files changed, 370 insertions(+), 53 deletions(-) diff --git a/booth/app.py b/booth/app.py index c547790..8f37bea 100644 --- a/booth/app.py +++ b/booth/app.py @@ -226,6 +226,8 @@ from booth.links import ( # noqa: E402 def human_dur(seconds: float) -> str: + if not math.isfinite(seconds): # as S5a fixup: int(nan) raises; say nothing we cannot know + return "—" s = int(seconds) if s <= 0: return "expired" diff --git a/booth/templates/_ask_inline.html b/booth/templates/_ask_inline.html index e51646b..90952d2 100644 --- a/booth/templates/_ask_inline.html +++ b/booth/templates/_ask_inline.html @@ -25,10 +25,10 @@ {% set skipped = a.answer and not picked %}
{% if picked %}✓ answered{% elif skipped %}— skipped{% else %}? your pick{% endif %} -

{{ q.prompt }}

+

{{ q.prompt }}

{% if picked %}

recorded: {{ qa.label }}{% if qa.notes %} — {{ qa.notes }}{% endif %}

{% elif skipped %}

left blank — pick one any time, or leave it{% if qa and qa.notes %}; note: {{ qa.notes }}{% endif %}

{% endif %} -
+
{% for o in q.options %}
{% else %} - {% if a.title %}

{{ a.title }}

{% endif %} + {% if a.title %}

{{ a.title }}

{% endif %} {% for q in a.questions %}{{ question(a, q, form_id, name_url) }}{% endfor %} {{ submit(a, form_id, name_url) }} {% endif %} diff --git a/booth/templates/base.html b/booth/templates/base.html index 02677ee..dc1a24e 100644 --- a/booth/templates/base.html +++ b/booth/templates/base.html @@ -30,6 +30,52 @@ } catch (e) {} })(); + {% block title %}The Booth{% endblock %} {# The two SVOS voices. display=swap and the system stacks in --font-sans / @@ -1496,45 +1542,5 @@
drop a folder into {{ data_dir }}{% if host %} · {{ host }}{% endif %}
- diff --git a/booth/templates/booth.html b/booth/templates/booth.html index 5ed942c..699fff6 100644 --- a/booth/templates/booth.html +++ b/booth/templates/booth.html @@ -44,7 +44,9 @@ textarea. #} {% macro marknotes(name_url, it, marks) -%} {% for m in marks if m.shape == 'note' %} -
+ {# no id: `mark-` names the panel's article, which is what the CLI's + `#mark-` links mean (booth-dev, 2026-09-28) #} +
{{ m.text }}
@@ -94,7 +96,8 @@ {# Promote or release without going back to the index. `next` keeps you on this page instead of bouncing you to /. #} {% if kept %} - +
diff --git a/docs/contracts/as_antislop.contract.md b/docs/contracts/as_antislop.contract.md index 6e013c2..27a67eb 100644 --- a/docs/contracts/as_antislop.contract.md +++ b/docs/contracts/as_antislop.contract.md @@ -162,6 +162,16 @@ The markup and CSS half of the interaction work. It changes no script behaviour - A why truncated with an ellipsis carries its full text in `title`. - A countdown of 48h or more rolls up to days (`6d 23h`, not `167h 12m`). - *Falsifiable:* `test_a_truncated_why_carries_its_full_text`, `test_human_dur_rolls_up_to_days`. +- **Fixup from booth-dev's gate** (a hulda bug-hunt plus heid's second voice, BRINGA, thread `01M3MVGQ7QSCCK8WT59TQ4J469`): + - The booth page's "★ kept — release" asks by name, as the Desk's release does. + - `WORDS` has no prototype, so a `data-confirm` of `__proto__` or `constructor` is an unknown word, and it asks. + - The confirm helper lives in ``, so its capture listener is registered before any form exists. A click during load is asked too; the inline `confirm()` it replaced had that property. + - `shown()` also marks U+2028, U+2029, U+200B–U+200D, U+2060 and U+FEFF. + - Derived ids take a `:`, which no ask id or question key can contain: `bk-ask--:prompt` and `bk-ask-:title`. `-prompt` or `-title` collided with valid keys. The asks chip still jumps to it by URL fragment; booth-dev's `test_the_chip_does_not_jump_to_a_mark_that_merely_shares_a_prefix` now looks the target up by `[id=…]`, since a `#` selector cannot hold a `:`. + - `human_dur` returns "—" for a value that is not finite, instead of raising. + - The tile's copy of a note drops its `id`, because `mark-` names the panel's article (booth-dev's ruling). + - The guards the gate found asserting source patterns now also hold on computed effects: the embed's rings are a solid, opaque 2px line under a host that removes outlines; the rings inside clipping containers compute to `-2px`; the withdraw × is measured on both axes; and question-level notes fields are named. + - *Falsifiable:* `test_no_id_repeats_on_any_page`, `test_release_on_the_booth_page_asks_by_name` (and its browser twin), `test_a_prototype_word_still_asks`, `test_the_confirm_helper_is_listening_before_the_body_exists`, `test_the_dialog_shows_hidden_breaks_and_zero_widths_visibly`, `test_human_dur_never_raises`, `test_rings_inside_clipping_containers_are_drawn_inside`, `test_the_embed_draws_visible_rings`, and the rows marked "S5a fixup" in `antislop.toml`. - **Existing rows this slice edits** (booth-dev's): two `r2_flow.toml` rows for the confirm helper now name `base.html`, where the helper moved. Their anchors are unchanged. - **Reported, not changed:** mark ids repeat across a tile and its aside (`mark-note-1`). The CLI prints `#mark-` links to them, so the fix is booth-dev's call. diff --git a/tests/mutations/antislop.toml b/tests/mutations/antislop.toml index ecead2d..e951129 100644 --- a/tests/mutations/antislop.toml +++ b/tests/mutations/antislop.toml @@ -473,15 +473,15 @@ new = """
""" -new = """

""" +old = '''

''' +new = '''

''' [[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 = """

""" -new = """

""" +old = '''

''' +new = '''

''' [[mutation]] label = "S5a a single-question fieldset without a legend" @@ -643,3 +643,126 @@ 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{}}""" + +# ---- S5a fixup: booth-dev's gate (hulda + heid BRINGA, thread 01M3MVGQ7QSCCK8WT59TQ4J469) + +[[mutation]] +label = "S5a fixup the booth page's release does not ask" +file = "booth/templates/booth.html" +test = "tests/test_antislop.py::test_release_on_the_booth_page_asks_by_name" +old = ''' + + data-booth="{{ name }}" data-confirm="release">''' +new = '''>''' + +[[mutation]] +label = "S5a fixup the booth page's release does not ask (browser)" +file = "booth/templates/booth.html" +test = "tests/test_antislop_browser.py::test_release_on_the_booth_page_asks_in_the_browser" +old = ''' + + data-booth="{{ name }}" data-confirm="release">''' +new = '''>''' + +[[mutation]] +label = "S5a fixup WORDS inherits from Object.prototype" +file = "booth/templates/base.html" +test = "tests/test_antislop_browser.py::test_a_prototype_word_still_asks" +old = ''' var WORDS = Object.create(null);''' +new = ''' var WORDS = {};''' + +[[mutation]] +label = "S5a fixup the confirm helper leaves " +file = "booth/templates/base.html" +test = "tests/test_antislop.py::test_the_confirm_helper_is_listening_before_the_body_exists" +old = '''' + '' # a host that removes rings + '

Report

') + page = browser.new_page() + page.goto(f"{base}/b/r/", wait_until="networkidle") + page.wait_for_selector(".bk-ask-go", timeout=10000) + for sel in (".booth-nav-home", ".bk-ask-go"): + ring = _keyboard_focus(page, sel) + alpha = page.evaluate(_ALPHA, ring["color"]) + assert ring["fv"] and ring["style"] == "solid" and ring["width"] == "2px" and alpha == 1, (sel, ring) + page.close() diff --git a/tests/test_embed_browser.py b/tests/test_embed_browser.py index 6ec6b09..963e0d2 100644 --- a/tests/test_embed_browser.py +++ b/tests/test_embed_browser.py @@ -489,7 +489,9 @@ def test_the_chip_does_not_jump_to_a_mark_that_merely_shares_a_prefix(browser, l target = page.locator(".booth-nav-asks").get_attribute("href") assert "batch2" not in target, f"the chip landed on the sibling mark: {target}" assert target.startswith("#bk-ask-batch") - assert page.locator(target).count() == 1 + # as S5a fixup: derived ids take a `:` (`bk-ask-batch:title`) so they cannot + # collide with a question key; a fragment may hold one, a #selector cannot + assert page.locator(f'[id="{target[1:]}"]').count() == 1 page.close()