clay-borg/evidence/CB-EV-0016-the-browser-is-a-client.md
tegwick a733826d95
Some checks failed
ci / check (push) Failing after 3s
CB-WP-0018 T03/T04: explanations, and window 1's verdict
T03: input::describe writes a sentence per legal command; data-descs
carries them in step with data-targets; the ghost already following the
pointer shows the one for whatever legal target is under it, so the
explanation lands beside the target with no overlay layer to keep
aligned. ADR-0010 D1 binds -- the page renders it, never composes it.

Both mutations INITIALLY SURVIVED because the fixture's Attack card had
exactly one target, where an off-by-one shift and a truncation are both
no-ops. CB-EV-0014's lesson one level in: a fixture too thin to express
a failure is how the failure survives. Two attack targets now, both red.

T04: chaos rate d4 -> d8, window 2 open at 12 declarations, retiring if
an override changes nothing twice running. Window 1's condition was NOT
met -- both overrides changed the outcome -- so the mechanism is kept.
The weakest part of the decision is that it is a rate change argued from
n=2, so window 2 carries a falsifier: no override at all is evidence the
rate went too far, not that the mechanism is healthy.

InnerLoop.md hit 401 lines and the loadability gate fired; the rationale
moved to InnerLoopReference.md, structurally, per the standing precedent
that limits are not raised.

CB-WP-0017 settled at $9.48/40 against $5.19/23 reported mid-flight,
83% higher. Six for six, always low -- read by re-running the instrument
at the moment of quoting, which is CB-EV-0015's correction applied for
the first time.

make all exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 02:24:13 +02:00

8.3 KiB
Raw Blame History

CB-EV-0016 — the browser is a client, and window 1's verdict

CB-WP-0018 T04. Measured 2026-08-03 at e8bb726+. Pass kind product, tier M (structural M — changes the loop's own constraints; chaos d4=3, no override). Declaration 1 of chaos window 2, opened by this pass.


1. A finished game was 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 before touching anything, by driving a real game to completion over HTTP:

move 5 accepted → ok
GET /           → [Errno 111] Connection refused

The game had ended normally — 5 rounds, 30 commands — and its entire result went to a terminal nobody was reading. next_choice only accepts connections inside a human decision point, so when play() returned the listener died and the page's post-ok reload was refused.

The browser was a second-class client. The outcome, the scores, the winners, and every error run_game can return were invisible to the only interface a player uses. Now: an 8,998-byte page reading "GROUND — game over … 30 commands, hash f6c890a65271", with the final table and the log.

It serves until the page posts done — the ending carries a "close — I have read this" control — with a 600 s linger, because a server that never exits is its own defect and a timeout would race a player reading the result.

2. The control had to be built twice, and the first was worthless

the_end_of_the_game_reaches_the_browser calls serve_end directly. It passes. Deleting the call from run_game left it green.

That is CB-EV-0012's finding recurring almost verbatim — "every link was tested and the chain was not" — and it survived one full round of mutation here before anyone noticed, because the mutation was run and the verdict was read as "no coverage gap" rather than "the test is in the wrong place".

a_real_game_played_to_its_end_leaves_the_ending_on_screen runs the real play() with a browser seat, drives a real game to its end over a real socket, and requires the last page to be the ending. Under the same mutation it goes red — and its failure message prints an empty page, which is precisely the symptom that was reported.

A weak assertion of mine, caught by itself. The first version grepped the ending page for location.reload. The page reuses SCRIPT, whose reload is guarded by t.indexOf('ok') === 0, and the ending endpoint answers closed — so the grep would have forced a second script into existence to satisfy a test rather than a requirement. That is the source-text control shape ADR-0010 D2 demoted three passes ago, reappearing in my own hands.

3. The card report, and what it actually was

Reported: "the cards I play by pulling them on a target will not be removed … we will need a discard pile."

A discard pile already existedsolution_discard on GroundState, SolutionDiscarded removing the card from the hand, and the page already rendering deck N remaining / discard …. Building one would have been building a thing that was there, and the only reason that did not happen is that the code was read before the work started.

What was being dragged are action cards. The five GROUND actions are not cards and are correctly never consumed; solution cards leave the hand at Resolve, because a selection is a face-down commit.

But the report pointed at something real. Measured live:

move hand discard
1 · Investigate → problem-2 2 cards none
2 · Investigate → problem-3 3 cards none
35 · Solve → problem-1 4 cards, unchanged none

Investigate draws, correctly. Solve was played three times and did nothing, three times, in silence — GR-A02's resolver continues when the problem is face-down, and legal_commands offers Solve on every face-up problem without consulting the hand.

Raised for ground-game, not decided here (INTENT defers game semantics): should SOLVE be selectable against a face-down problem, or against a suit the seat cannot match? A face-down commit you cannot fulfil is a plausible bluff in a commit/reveal game with DARVO, which is exactly why it is not this repo's call.

4. The log, and the limit of what it can honestly say

bot::JournalApplied { actor, command, events } appended by the driver through the new play_journaled; play delegates with None, so nothing existing changed. BotGame.events is the same information but only after play returns, which is no use to a page rendered mid-game.

Phrased with record::to_step, so what the player reads is what the scenario file will say, and all 29 GroundEvent variants render in words rather than {:?}.

A command that produced no events says no effect, and the mutation removing that branch goes red.

The honest limit: the reported SOLVE case does not render as no effect, because the SOLVE resolves inside the system's resolve command, which does produce events for other seats. The player now sees the selection and sees no claim follow it — a large improvement on silence, but still an inference. Making it explicit would require the renderer to decide why a rule did nothing, which is a second implementation of the rules and is what this task's own control forbids. Left as an inference deliberately.

5. The explanation, and two mutations that a thin fixture defeated

Every advertised target now carries a sentence Rust wrote for that pair (data-descs, in step with data-targets), shown at the pointer while dragging over it. ADR-0010 D1 binds: the page renders it, never composes it.

Both mutations — showing a neighbouring pair's text, and letting targets and descriptions fall out of step — initially survived, because the test fixture's Attack card had exactly one target, where an off-by-one shift and a truncation are both no-ops.

That is CB-EV-0014's lesson again, one level in: a stub too thin to express a failure is how the failure survives. The fixture now offers two attack targets on purpose, and both mutations go red.

6. Window 1's verdict, and a rate change on n=2

Twelve declarations, two overrides, one each way, both changed the outcome — so window 1's retirement condition was not met and the mechanism is kept. The full table is in InnerLoopReference.md §Chaos roll.

Rate dropped d4 → d8; window 2 opened at 12 declarations; new retirement condition: retire if an override changes nothing twice running.

The weakest part of this pass, stated plainly: it is a rate change argued from n=2. The alternative — keep d4 for a second window and decide with four points — was live, and was rejected only because a quarter of all declarations is a large standing tax to pay for evidence. So window 2 carries a falsifier: if it produces no override at all, that is evidence the rate went too far, not that the mechanism is healthy. A window that cannot fire cannot be evaluated, which is the exact failure d10 had.

7. Cost

pass kind responses cost $/response
CB-WP-0016 product 64 $14.93 0.233
CB-WP-0017 product 40 $9.48 0.237
CB-WP-0018 product provisional — not quoted

Read by re-running make status at the moment of writing, which is CB-EV-0015 §6's correction applied for the first time: quoting a figure remembered from earlier in a session defeats the rule even when the boundary is right. CB-WP-0017 was reported at $5.19/23 mid-flight and settled at $9.48/40 — 83% higher. Six for six, always low.

Meta budget 0% [ok], all three trailing passes product.

8. Open

  • INTENT stage 1: the human check. Three runs, three defects no test could reach. Everything in this pass is verified by tests, mutation and a live socket; nothing perceptual is.
  • For ground-game: should SOLVE be selectable against a face-down problem or an unmatchable suit? §3.
  • The self-quoting rule now has both halves recorded but is still not written into the loop spec.
  • AM-4b's scope defect (408,237 uncounted lines) and its proc-macro share.
  • python3 as a toolchain dependency was never argued.
  • AM-4a cannot survive stage 2 — 1,741,979 against 161,000.
  • ADR-0007 D3's acquisition rule and D5 remain unratified; ADR-0010 rests on the latter.