Skip to content

OHH Frame Projector + the UI Stability Trilogy

Status: accepted + shipped 2026-07-20 (PR #59). Plan 3 of 3 of ui-stability. Full ADR: .indusk/planning/ui-stability/adr.md (D3 + D5). Plan archive: .indusk/planning/archive/ohh-frame-projector/.

What was decided

A pure projector turns a recorded OHH hand into the paced game_state frame sequence the felt would have received live, feeding the SAME BratPokerTable. One machine, three payoffs:

  • Golden replay regression engine — recorded hands replayed through the real felt under fake timers, asserting the choreography (the client-sequence class where 4 of the 6 audit specimens lived). Every future felt bug becomes a permanent fixture.
  • Player-facing replay — the ✋ hand-history modal's ▶ watch mode.
  • The purity property — every projected frame passes the same D1 frame contract live frames obey.

Plus the D3 writer-integrity rule (ohh_show_cards_folded — a record that shows a folder's cards fails at the observatory boundary, before it becomes a wrong fixture) and the D5 registry gate (packages/game-ui/FELT-REGISTRY.md, repo-tracked so PRs can see it: a category-(c) felt behavior without a pinning test is a rejected PR).

Key tradeoffs accepted

  • Real fixtures over synthetic. Captured-from-live records pin the OHH amount semantics (INCREMENTAL chipFlow) a synthetic fixture would encode wrong. Cost: fixtures must be sanitized; benefit: the projector's simulation is validated against ground truth.
  • jsdom structure-level asserts, not pixels. The golden suite asserts style vars (the class that caught PR #35's dead CSS vars), not rendered animation. Real-browser feel remains human smoke — now at a known sha.
  • The projector is structurally blind to the freeze class (EK71HB produces no OHH) — that is felt-liveness's territory (plan 1). The projector owns the client-choreography class only.

The trilogy (all three shipped 2026-07-20)

  1. felt-liveness-contract — hands conclude or freeze loudly; the frame contract; the watchdog. No hand wedges silently; no frame lies.
  2. verified-done — the deployed sha is observable (service.version, /api/version, the page tag); deploy.completed un-ghosted; the three-predicate done ritual. You KNOW which build is live.
  3. ohh-frame-projector — client choreography is pinned + replayable; the registry gate. Regressions become fixtures instead of memories.

The debugging mental model that ties them together is the UI debug routing guide (apps/docs/guide/ui-debug-routing.md): classify modeled vs unmodeled, then walk failure surfaces S1–S8, stopping at the first surface whose model disagrees with reality, and end every fix by strengthening the model one layer earlier.

Falsification found (both fixed in scope)

  • The projector was blind to the exact freeze class the trilogy exists for: blind-all-in runouts (zero voluntary actions) showed their blinds on NO frame — no sweep fuel. All three original fixtures dodged it by accident. blind-allin-runout.json is now the permanent EK71HB-class fixture.
  • The projection memo keyed on options object identity → an inline literal under the live table page's re-renders would have stalled the replay forever. Fixed with a canonical-serialization key.