CB-WP-0024 T01/T02: the ending control says what it does, and the piles are objects

T01. The maintainer asked why the button says "I need to read this" and
why nothing closes. Two defects behind one control: the label described a
reading while the control STOPS THE SERVER (hotseat.rs reads `done` and
breaks its loop), and acknowledging it changed nothing on screen -- the
tab kept a live table and a `play again` pointing at a closed port.

Label is now "end session -- stops the game server". The reply says the
session has ended and the tab can be closed. The script seals the page on
a `closed` reply: removeAttribute('data-drop') on every control, so they
stop being droppable by the same rule that made them droppable. CSS is how
that reads, not the mechanism. removeAttribute rather than
setAttribute(_, null) -- the latter writes the truthy string "null" in a
browser, so the control would stay live while the stub called it sealed.

THE REPLY PATH HAD NEVER BEEN EXECUTABLE IN A TEST. jsrun's fetch stub
returned {then: function(){return this}}, which never invoked its
callbacks, so every line of the script reacting to the server was
unreachable from every test in this project. That is why the defect
survived: a page ignoring the server looked identical to one acting on it.
The stub now delivers a real then-chain and gesture_with_reply reports
which controls survive. The seal is mutation-proven -- deleting the
`closed` branch turns exactly one test red -- and a negative control
asserts an `ok: dealing` reply does NOT seal, since a seal that fired on
every reply would pass the first test and break `play again`.

T02. Draw and discard drawn as offset stacks with their counts. The
shuffle question the task required answering is settled and the answer is
that it already works: games/ground/src/lib.rs:1419-1435 implements the U4
default -- deterministic reshuffle of the discard seeded from seed ^ round,
skip the draw if both are empty -- and ground-game CONFIRMED U4 on
2026-08-03. A ruled rule, not an invented one, nothing to raise. The event
already reads out in the log; what the piles add is the state before it
fires, which is derivable from the view. A claim that a reshuffle HAS
happened would not be, and is not made.

The coverage gate caught its own probe going stale when the "17 remaining"
text was replaced. The count now lives in the pile's <title> -- a stable
probe and what a screen reader announces, where the on-canvas numeral
could be any number on the page.

39 tests pass; cb-play 22 including play_again_deals_a_second_game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-05 17:23:09 +02:00
parent 5f9c2839bb
commit 1edb10d5a3
5 changed files with 443 additions and 15 deletions

View file

@ -2,7 +2,7 @@
id: CB-WP-0024
kind: product
title: "The table you can watch: the piles, the other seats' moves, the score that carries"
status: ready
status: active
state_hub_workstream_id: "5f17b6f9-cd4b-4c31-a8af-712313149cf2"
---
@ -47,7 +47,7 @@ except where task T04 says so explicitly.
```task
id: CB-WP-0024-T01
status: todo
status: done
priority: high
state_hub_task_id: "acb4231c-35df-490f-93fd-be71c0abf1dc"
```
@ -75,11 +75,36 @@ because a control that can no longer work must not look like it can.
- the label change must be mutation-visible: assert the rendered text, so
reverting the wording turns a test red.
**Done 2026-08-05.** Label is now `end session — stops the game server`;
the reply is `closed — the session has ended and the server has stopped.
You can close this tab.`; and the script **seals** the page on a `closed`
reply — `removeAttribute('data-drop')` on every control, so they stop
being droppable by the same rule that made them droppable. `sealed` CSS is
how that reads, not the mechanism.
**`removeAttribute`, not `setAttribute(_, null)`** — the latter writes the
literal string `"null"` in a browser, which is truthy, so the control
would stay live while the stub reported it sealed.
**The reply path had never been executable in a test.** `jsrun`'s `fetch`
stub returned `{then: function(){return this}}`, which never invoked its
callbacks — so every line of the script that reacts to what the server
said was unreachable from every test in the project. **That is why this
defect survived**: a page that ignores the server looked identical to one
that acts on it. The stub now delivers a real then-chain, and
`gesture_with_reply` reports the page's surviving controls.
Three tests, and the seal is mutation-proven: deleting the `closed` branch
turns exactly one red. The negative control (`ok: dealing` must NOT seal)
exists because a seal that fired on every reply would pass the first test
and break `play again`. `cb-play`: 22 passed, including
`play_again_deals_a_second_game`.
## Task: the piles are objects on the table
```task
id: CB-WP-0024-T02
status: todo
status: done
priority: high
state_hub_task_id: "37b16a87-e791-4c85-9f60-671812f6dd2e"
```
@ -107,6 +132,33 @@ stop.
- the shuffle question is answered in the task record with the line number
that settles it, either way.
**Done 2026-08-05.** Both piles are drawn as offset stacks with their
counts, replacing the text line. Depth suggests *a pile exists*; the
number is the truth — 17 cards is not seventeen rectangles.
**The shuffle question is settled and the answer is that it already
works.** `games/ground/src/lib.rs:1419-1435` (`draw_solution`) implements
the **U4 default**: deck empty → deterministic reshuffle of the discard
via a `DeckReshuffled` event seeded from `seed ^ round`; both empty → skip
the draw. ground-game **confirmed U4 on 2026-08-03**, so this is a ruled
rule, not an invented one. Nothing to raise.
The event already reads out in the log (`hotseat.rs:374`). What the piles
add is the *state before it fires*: deck 0 with a non-empty discard draws
an arrow and says **"shuffles in on next draw"**. That is derivable from
the view; a claim that a reshuffle *has happened* would not be, and is not
made.
**The coverage gate caught the probe going stale** — removing the
`17 remaining` text broke `solution_deck_len`'s classification. Fixed by
putting the count in the pile's `<title>` (`draw pile: 17 remaining`),
which is both a stable probe and what a screen reader announces; the
on-canvas numeral alone could be any number on the page.
Three tests: counts come from the view, an empty discard renders as an
empty pile rather than vanishing, and the reshuffle notice appears only
when the deck is actually out.
## Task: what the other seats played, on the table
```task