From the anti-slop run (design-dev, 2026-09-28). Faded is not legible: opacity divides whatever contrast a line had. - Review arrows: the chip under the thin chevron is 82% dense, not 60%; the glyph now clears 7:1 over a white stage by colour (was 3.84:1), and reads at pixel level where the detector sampled a 2.9:1 median. - Filmstrip numbers, the marks' state stamp and the inline ask's state tag are labels at 11px (were 9.5 / 10.5 / 10.5px). - The Desk's section rules, the board note and the bench note are sentences at 12px. - Retired benches: no opacity; the link and URL take --text-muted. - Embed chrome: answered-ask details and the "recorded:" line inherit the host's text colour at full strength (the embed cannot know the host's palette); the notes placeholder inherits it at 75%, not the UA grey. Folded from the heid bug-hunt: the embed's ask title no longer fades either (Q9), and the legibility claims 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); the browser can. Folded after the first gate run: the flagged tray's number, the tile's "flagged" stamp and compare's A/B badge were still under the 11px label floor; they take --size-micro too. Two film-number rows are re-anchored on the .film-ord selector: the tray's line is now identical to it, and the runner mutates the first match. Contract: as_antislop S2. Falsifiers: antislop.toml 34/34 proved (S1+S2); all 12 tables 314/314 proved on this tree.
99 lines
9.4 KiB
Markdown
99 lines
9.4 KiB
Markdown
---
|
||
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 `<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 to S6
|
||
|
||
These are added as each slice is staged:
|
||
- S3, phone layouts;
|
||
- 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`).
|