Files
booth/docs/contracts/as_antislop.contract.md
T
vh 44b80e6d9a fix(as-S3): phone layouts — nothing overprints, no word set narrower than itself
From the anti-slop run (design-dev, 2026-09-28). Measured in a real browser
at 390x844 (tests/test_antislop_browser.py), because a layout claim read off
a stylesheet is a guess.

- Bench rows wrap at <=600px (state + name/URL, then who/when/actions); the
  name's column was squeezed to ~53px and overprinted the owner and date.
- The board head and the benches head drop their note under the count, so
  "33 links · 1 pinned" / "3 benches" keep one line.
- An inline doc's bar wraps: the name takes the full width and breaks only
  where it must; it was set one word wide.
- A file tile's download link starts below the ordinal badge.
- The review and compare stages drop the tagline at <=600px (the server
  marks them `page-stage` on <html>), so the header is one line; the Desk
  keeps its tagline (the test's negative control).

Not changed, with reasons in the contract: `.vname` already ellipsises, and
the filmstrip's clipped edge frame is the scroller's "more" cue.
Contract: as_antislop S3. Falsifiers: antislop.toml 41/41 proved (S1-S3);
all 12 tables 321/321 proved on this tree.
2026-09-28 13:30:31 -07:00

118 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)"
- "booth/templates/base.html, view.html, compare.html; tests/test_antislop_browser.py (S3)"
- "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 `<time>` whose `datetime` carries the value exactly as stored:
- a pick's answer line and a memo's line (`_marks.html`);
- the inline ask's state tag (`_ask_inline.html`, so the embed chrome inherits it);
- the link board's row time (`booth.html`).
- *Falsifiable:* the marks page, the lightbox's verdict aside, the embed fragments and the board carry no visible `HH:MM`, no `T08:48`-shaped stamp and no IP: `test_rendered_marks_use_the_house_clock`, `test_board_rows_use_the_house_clock`, `test_embed_fragment_uses_the_house_clock`. Removing the filter from any one of those templates turns its test red.
- **The date tooltips follow suit.** `date_stamp` (the `title` of every created/updated `<time>`) renders `YYYY-MM-DD HHMM`.
- *Falsifiable:* `%H:%M` in `date_stamp` fails `test_date_stamp_is_house_form`.
- **Folded from the heid bug-hunt** (panel 4/4, thread `01M3MGPFKWBX0SJK5HFE0P3AFM`):
- `clock` converts a number inside its guard: an int past float range was an `OverflowError` (Q1).
- A date or an ISO week renders its day and no invented `0000` (Q8).
- `byline` also hides an address dressed as `addr:port`, `[v6]:port` or `addr/prefix`, or behind invisible characters (Q7).
- The board row's author goes through `byline` like every other surface (Q5).
- *Falsifiable:* the rows marked Q1, Q5, Q7 and Q8 in `antislop.toml`.
- **Refuted, with the reason:** a malformed answer missing `unanswered` does not 500 the marks panel (Q3). The Booth's Jinja uses the default `Undefined`, whose `|length` is 0; the no-op fix was reverted when its falsifier stayed green. `test_a_malformed_answer_costs_its_line_not_the_page` stays, as a guard against a switch to `StrictUndefined`.
- **Accepted as known risk, with reasons:**
- Zone-less mark stamps are read as local by `clock` and as UTC by the ordering path (Q4). No writer produces one: `now_stamp` and the legacy import both stamp with `.astimezone()`. Only a hand-edited file could.
- A board time inside the spring-forward gap renders the normalised hour (Q6). No clock can write a local time that does not exist.
**Out of S1:** the CLI keeps writing its board rows as it does today. The board is a multi-writer file other sessions parse, so its storage form is not changed; `clock` reads both forms.
## S2 — legibility
**The rule.**
- Faded is not legible. A de-emphasised line is quieter by size, weight or a muted colour, never by `opacity`: opacity takes whatever contrast the line had and divides it.
- Labels are 11px or larger (`--size-micro`).
- A sentence-like line is 12px or larger (`--size-caption`).
- **Review arrows on a light stage.** The ‹ › glyph sits on a translucent dark chip. At 60% the chip let a light stage through, and the thin glyph sampled at a median of 2.9:1 (the detector's pixel method; by colour it is about 4:1). The chip is at least 80% dense, so the glyph clears 7:1 over the lightest stage by colour, and reads at pixel level too.
- *Falsifiable:* a chip back at 60% fails `test_review_arrows_hold_over_a_white_stage`, which composites the glyph over the chip over white.
- **The filmstrip numbers are labels:** `--size-micro`, not 9.5px.
- *Falsifiable:* 9.5px fails `test_film_numbers_meet_the_label_floor`.
- **Retired benches keep their contrast.** The row carries no `opacity`. The link and URL take `--text-muted`, and the state word says RETIRED.
- *Falsifiable:* `opacity:.5` back on the row fails `test_retired_benches_are_not_faded`.
- **The marks' state stamp** (`? open`, `✓ answered`) is a label at `--size-micro`, not 10.5px.
- *Falsifiable:* `test_mark_state_meets_the_label_floor`.
- **The inline ask's state tag** (the embed chrome's `✓ answered 28 Sep 0848`) is a label at 11px, not 10.5px. S1's `<time>` made the detector measure it on its own.
- *Falsifiable:* `test_the_ask_tag_meets_the_label_floor`.
- **Hint lines are sentences:**
- the Desk's section rules (`.desk-rule`: "oldest question first", "running things");
- the board's note;
- the bench head's note.
They sit at `--size-caption`.
- *Falsifiable:* `test_hint_lines_meet_the_sentence_floor`.
- **The embed chrome fades nothing.** An answered ask's option details and its "recorded:" line inherit the host page's own text colour, with no `opacity`, so they carry the host's contrast whatever the host is. The embed cannot know the host's palette; its own palette follows the Booth theme, not the report. The notes field's placeholder inherits that colour at 75%, where it had been the browser's grey (3.5 to 4.3:1 on dark).
- *Falsifiable:* `test_embed_fades_nothing`.
- **Folded after the first gate run:** three more labels were below the 11px floor, and they are now `--size-micro` like the rest. They are the flagged tray's number (10px), the tile's "flagged" stamp (10.5px) and compare's A/B badge (9.5px). The report's list had named only the film numbers. *Falsifiable:* the same computed-style test, and three more rows.
- **Folded from the heid bug-hunt.** The embed's ask title no longer fades either (`opacity:.62` on `.bk-ask-title` contradicted "nothing fades", Q9). The claims above are also held on the browser's computed style (`tests/test_antislop_s2_browser.py`): a stylesheet grep cannot see a later rule in the cascade (`font-size:1px`, `color:transparent`, `filter:grayscale`, a placeholder at `opacity:0`), and the browser can.
## S3 — phone layouts
**The rule:** at phone width (≤600px), no text overprints other text, and no single word is set in a column narrower than itself. These claims are measured in a real browser at 390×844 (`tests/test_antislop_browser.py`), because a layout claim read off a stylesheet is a guess.
- **Bench rows wrap instead of squeezing.** At ≤600px a row wraps. The state word and the bench (name over URL) take the first line; who, when, the state buttons and × take the second, indented under the name.
- *Falsifiable:* without the wrap, the name, owner and date boxes intersect: `test_bench_rows_do_not_overprint_on_a_phone`.
- **An inline doc's name keeps its line.** At ≤600px the doc bar wraps. The name takes the full width and breaks only where it must (`overflow-wrap:anywhere`, not `word-break:break-all`); the actions wrap under it.
- *Falsifiable:* `test_doc_name_keeps_a_readable_line_on_a_phone`.
- **The link board's headers stay compact.** At ≤600px the note drops under the count, so the count ("33 links · 1 pinned", "3 benches") stays on one line, in both the board head and the benches head.
- *Falsifiable:* `test_board_head_stays_compact_on_a_phone`.
- **A file tile's number clears its download link.** The ordinal badge sits in the tile's top-left corner, so a file tile's link starts below it.
- *Falsifiable:* `test_file_tile_number_clears_the_download_link`.
- **The review and compare pages keep their header to one line on a phone.** At ≤600px they drop the tagline (every other page keeps it), so the header is the brand plus the theme toggle and the stage starts near the top. The class that scopes this is set by the server on `<body>` (`<html>` carries `data-booth`, which the reveal scripts and their tests pin exactly).
- *Falsifiable:* `test_review_header_is_one_line_on_a_phone`, which also checks that the Desk keeps its tagline.
**Measured, not changed** (the detector's rows that are misreads here):
- `.vname` already ellipsises; the detector measures the clipped inner width.
- The filmstrip clips its next frame at the edge on purpose: the clipped frame is the "there is more" cue of a horizontal scroller.
## S4 to S6
These are added as each slice is staged:
- S4, reading measure;
- S5, interaction and screen readers;
- S6, the operator's rulings (tagline, needs-you stripe, matte brand dot, the Wipe-now stripe on `::before`).