Files
booth/docs/contracts/as_antislop.contract.md
T
vh 54f3531833 fix(as-S2): legibility — nothing fades, labels 11px, sentences 12px
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.
2026-09-28 13:30:31 -07:00

9.4 KiB
Raw Blame History

contract_version, status, module, purpose, depends_on, language, complexity, touches, assumptions
contract_version status module purpose depends_on language complexity touches assumptions
0.1 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. the Booth's rendered surface: filters in booth/app.py, templates, booth/static/embed.js 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.
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`).
python + jinja low per slice
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
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).