--- contract_version: "0.1" status: "PROPOSED 2026-09-28 by design-dev. The operator ordered the fix slices from the anti-slop run (booth `booth-antislop`, report `~/.local/share/design-dev/research/booth-antislop-2026-09-28.md`) in design-dev's session: \"go with your recommendations, push, start the fix slices\". Each slice is staged as its own ref (`design-dev/antislop-sN`) for booth-dev's gate: suite, mutation tables and a bug-hunt." module: "the Booth's rendered surface: filters in booth/app.py, templates, booth/static/embed.js" purpose: "Fix what the anti-slop run found (the Impeccable detector at 1280 and 390 in light and dark, plus a Vercel Web Interface Guidelines review), one slice at a time, without moving any invariant." depends_on: - "app.py: the Jinja Environment and its filters (`human_dur`, `date_iso`, `date_stamp`, `date_day`, `date_ago`); the note/answer routes that record `who = request.client.host`." - "marks.py: `Mark.created`, `Mark.by`; asks.py: `answer.answered_at`, `answer.answered_by` (ISO strings with microseconds and an offset, written by `now_stamp`)." - "links.py: `parse_link_entries` -> `when` (the text a `booth link` row carries, today `YYYY-MM-DD HH:MM`)." language: "python + jinja" complexity: "low per slice" touches: - "booth/app.py (filters)" - "booth/templates/_marks.html, _ask_inline.html, booth.html (S1)" - "booth/templates/base.html, view.html; booth/static/embed.js (S2)" - "tests/test_antislop.py; tests/mutations/antislop.toml" assumptions: - "ONE VIEWER, on this box: local time is the operator's time (US Pacific), as the existing date filters already assume." - "Stored data does not change shape. Every slice changes only what is RENDERED: `.marks.json`, `links.md` and the answer records keep their exact bytes." - "Hardening of `render_doc` (raw HTML in docs) and front matter are booth-dev's, by agreement on 2026-09-28; this contract does not touch `render_doc`." --- # The anti-slop fix slices The run found that the Booth is sound on desktop and has a set of problems a viewer feels: clock times that break the house form, layouts that break at phone width, controls you can barely see in the light theme, and keyboard and screen-reader plumbing. The slices below fix them in an order that keeps each ref small enough to gate. Every slice keeps the six invariants (CLAUDE.md), and in particular: - the server renders every state, and scripts only place it; - autoescape stays on; - every ordered surface keeps its stated order; - blur honesty holds. ## S1 — the house clock **The rule** (operator convention, 2026-09-24): a clock time the operator reads is 24-hour local time (US Pacific), written as four digits with no colon (`0848`). Raw ISO stamps, `HH:MM`, microseconds, offsets and a poster's IP address do not appear in visible text. - **One filter decides the visible form: `clock`.** - It takes an ISO-8601 string (with or without microseconds and an offset), an epoch number, or the link board's `YYYY-MM-DD HH:MM`. - It returns `D Mon HHMM` in local time (for example `28 Sep 0848`), with the year after the month only when it is not the current year (`6 Sep 2025 2335`). - A value it cannot read is returned **as given**, never a guess and never an exception: the Desk and the board render many rows in one response, and one bad stamp must not 500 the page. An empty value returns `""`. - *Falsifiable:* a `clock` that formats `%H:%M` fails `test_clock_forms`. A `clock` that raises on garbage fails `test_clock_never_raises`. - **One filter decides who is shown: `byline`.** It returns the recorded `by` / `answered_by` unless it parses as an IP address (v4 or v6), in which case it returns `""`. The stored value is unchanged; the u2 contract still records the client host. - *Falsifiable:* a `byline` that passes IPs through fails `test_byline_hides_addresses`. - **Where the filters apply.** Every visible stamp goes through `clock` and every byline through `byline`, and each clock sits in a `