Declare CB-WP-0018: the browser is a client, tier M

Provenance: the maintainer reported 'after some time i get an empty page
back. I guess the game crashes or ends but that is unclear as the ui
disappears.' Reproduced by driving a real game to completion over HTTP:
move 5 accepted, then GET / -> Connection refused. The game ENDED
normally, 5 rounds and 30 commands, and its whole result -- coalitions,
scores, winners, hash -- went to the terminal. next_choice only accepts
connections inside a human decision point, so when play() returns the
listener dies and the post-ok reload is refused. A crash and a win
render identically: nothing. Same class as CB-WP-0016's silent drop.

Also carries the chaos rate change CB-EV-0015 owed to the next
declaration (d4 -> d8, second window of 12), which is what makes this
structurally M. Rolled at the old d4=3, no override, because a rate
changes when the decision lands and not retroactively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-02 22:59:05 +02:00
parent 332d8f1bba
commit e8bb7263f2

View file

@ -0,0 +1,173 @@
---
id: CB-WP-0018
kind: product
title: "The browser is a client: game over, a log, and where a drop goes"
status: todo
---
# Purpose
```
structural tier M (changes how the loop constrains its own operation:
the chaos rate d4 → d8 and a second calibration
window, owed by CB-EV-0015 §5)
chaos d4 = 3 → no override
declared tier M
```
**Declaration 1 of the second chaos window** — opened by T04 below, at the
rate T04 sets. This declaration was rolled at the old d4, because the rate
changes when the decision lands, not retroactively.
## The defect: a finished game is indistinguishable from a crash
Reported: *"after some time i get an empty page back. I guess the game
crashes or ends but that is unclear as the ui disappears."*
Reproduced by driving a real game to completion over HTTP:
```
move 5 accepted → ok
GET / → URLError: [Errno 111] Connection refused
```
The game **ended normally** — 5 rounds, 30 commands — and everything it
produced went to the terminal:
```
coalition [P2] score 0
coalition [P3] score 0
winners
game over — 30 commands, hash f6c890a65271
```
The browser got nothing, because `next_choice` only accepts connections
*inside* a human decision point. When `play()` returns, `run_game` writes
the outcome to stdout and the process exits; the listener dies and the
page's post-`ok` reload is refused.
**The browser is a second-class client.** Every terminal outcome — the
result, the scores, the winners, and every error `run_game` can return —
is invisible to the only interface a player is actually using. A crash and
a win render identically: nothing.
This is the same defect class as CB-WP-0016's silent drop. The system
refuses to say what happened and the player is left to infer it.
## Task: serve the end of the game
```task
id: CB-WP-0018-T01
status: todo
priority: high
```
When `play()` returns — **`Ok` or `Err`** — the browser must be told, on
the page, in terms a player understands.
- **A win** shows the outcome the spectator projection already carries:
totals, threshold, coalitions, mastery, winners.
- **An error** shows that the game ended badly and why. `run_game`
currently returns `Err(String)` to a terminal nobody is reading.
- The final page must **not** auto-reload into a refused connection.
The listener has to outlive the game, which it does not today. Decide how
it ends — a timeout, an explicit close, or serving until interrupted — and
say why; a server that never exits is its own defect.
**Controls:** a test that plays a game to its end through the socket and
asserts the final GET returns a page naming the outcome, **not** a refused
connection. And the error path, driven by a game that fails, asserting the
reason reaches the page.
## Task: a game log, and what to do next
```task
id: CB-WP-0018-T02
status: todo
priority: high
```
Requested: *"a game log about the events that have been generated and a
hint of what the next move/options are."*
The events exist — `validate` produces them and `record::to_scenario`
already turns them into a scenario file. The page shows none of them, so a
player sees state change with no account of why.
- **The log** is the event stream, in the recorder's vocabulary. Do not
invent a fourth phrasing: `cb-play`'s `describe()` exists precisely so
*"what the player reads is what the scenario file will say"*, and the
HTML renderer currently uses `{c:?}` Debug instead. Reuse it or say why
it cannot be reused.
- **The hint** is what the seat may do now, which is `legal` — already in
hand, already rendered as cards by CB-WP-0017. The hint is the *step*
context that makes those cards make sense: what the round is waiting
for, and what happens when it stops waiting.
**Control:** the log must be derived from the events the aggregate
produced, not re-narrated from the state. A log that describes the state
after the fact is a second implementation of the rules and will drift.
## Task: say where a drop goes, and what it means
```task
id: CB-WP-0018-T03
status: todo
priority: high
```
Requested: *"an overlay arrow pointing to the droptarget element with an
explanation showing up besides it, when we drag on it."*
CB-WP-0017 marks every legal target. What it does not do is say what any
particular drop **means** — the player sees five outlined boxes and must
still guess which one does what.
So the explanation is the substance and the arrow is the delivery. Both
need the meaning to exist as data first, and **ADR-0010 Decision 1 binds
here**: the description of what `action-attack → seat-1` does must be
written by Rust into the page. The script may render it on hover; it may
not compose it.
**Controls:** every advertised target carries a description, and the
description a target shows is the one Rust wrote for *that* pair — a
mutation that shows a neighbouring pair's text must go red. The set
equality property from ADR-0010 D2 must still hold.
## Task: close the chaos change, and the evidence
```task
id: CB-WP-0018-T04
status: todo
priority: high
```
CB-EV-0015 §5 recommended and did not make the change, on the grounds that
it alters how the loop constrains its own operation and was therefore owed
a tier-M declaration. This is it.
**Make it or reject it**, in `specs/InnerLoop.md` and `gates.toml`:
- rate **d4 → d8**;
- a **second window of 12** declarations;
- new retirement condition: *retire if an override changes nothing twice
running*.
The argument to test rather than assume: both overrides were informative
*because they were rare*. If that is right the rate should fall; if the
real driver was that they landed on passes with something to find, the
rate is irrelevant and the recommendation is wrong. Say which, and note
that n=2 is thin evidence for a rate change — including the possibility
that the honest answer is to keep d4 for another window and decide with
four data points instead of two.
Then `evidence/CB-EV-0016-*.md`:
- **What the maintainer's three reports have cost and bought**, now that
there are three in a row and every one found something no test did.
- **Whether stage 1 closes.**
- **Quote CB-WP-0017's cost by re-running the instrument**, not from
memory — CB-EV-0015 §6 found that quoting a remembered figure defeats
the rule even when the boundary is right, and this is the first pass
that can apply that correction.