diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index 3b7c3e9..ac9a5ed 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -92,6 +92,13 @@ pub const SCRIPT: &str = r#" // No game vocabulary here (ADR-0007 D5). 'closed' is a server // lifecycle word, exactly like the 'ok' branch below it. function seal() { + // The WHOLE page goes inert, not only the controls. A greyed-out + // button beside a full-colour table still reads as a live game with + // one broken control; the session has ended and everything on screen + // is now a record of it. + if (document.body && document.body.classList) { + document.body.classList.add('sealed-page'); + } var all = document.querySelectorAll('[data-drop]'); for (var i = 0; i < all.length; i++) { all[i].classList.add('sealed'); @@ -264,6 +271,11 @@ h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf} styling alone would leave a dead control that still looks alive to anything reading the DOM. */ .sealed{opacity:.3;cursor:default;pointer-events:none;filter:grayscale(1)} +/* The session has ended. Everything on screen is a record of a game that + is over, so the whole page says so -- not one greyed button beside a + live-looking table. */ +.sealed-page{filter:grayscale(.9);opacity:.5;pointer-events:none;transition:opacity .2s} +.sealed-page #cb-status{filter:none;opacity:1;color:#fc9} /* CB-WP-0027 T02. `minmax(0,...)` on both tracks, because a grid child defaults to min-content width and the SVG table would refuse to shrink, pushing the meta column off-screen instead of narrowing. @@ -1341,7 +1353,9 @@ pub fn ending( s.push_str(""); } // Observations 6 and 7: the controls and the full log belong beside - // the result, not under it. + // the result, not under it. And the controls go BELOW the log — you + // read what happened, then decide what to do next. + log_section(&mut s, log); let _ = write!( s, "
\ @@ -1349,7 +1363,6 @@ pub fn ending(
end session \u{2014} stops the game server
\
", ); - log_section(&mut s, log); s.push_str(""); let _ = write!( s, diff --git a/crates/cb-render-html/src/jsrun.rs b/crates/cb-render-html/src/jsrun.rs index 82b9bcf..490c5e8 100644 --- a/crates/cb-render-html/src/jsrun.rs +++ b/crates/cb-render-html/src/jsrun.rs @@ -55,10 +55,18 @@ pub fn scripts(html: &str) -> Vec { const DOM: &str = r#" var __handlers = {}; var __status = { textContent: "" }; +// The body carries a real classList, because `seal` marks the whole page +// inert on it (CB-WP-0028). A stub without one would make "the session +// visibly ended" untestable -- and CB-WP-0016 established that a stub too +// thin to express a failure is how the failure survives. +var __bodyCls = {}; var __body = { children: [], appendChild: function (n) { this.children.push(n); n.parentNode = this; }, removeChild: function (n) { var i = this.children.indexOf(n); if (i >= 0) { this.children.splice(i, 1); } - n.parentNode = null; } }; + n.parentNode = null; }, + classList: { add: function (c) { __bodyCls[c] = true; }, + remove: function (c) { delete __bodyCls[c]; }, + contains: function (c) { return !!__bodyCls[c]; } } }; var __all = []; // A DOM node with a real classList and real attributes. CB-WP-0016 found @@ -169,6 +177,7 @@ function __liveKeys() { return out.sort().join(','); } function __statusText() { return __status.textContent; } +function __bodySealed() { return !!__bodyCls['sealed-page']; } "#; /// Run the document's scripts, then a pointer gesture, and report what @@ -221,6 +230,11 @@ pub fn gesture(html: &str, down: &str, up: &str) -> Result, String> /// Returns `(live drop keys, status text)` — the keys still droppable /// once the script has handled the server's answer. /// +/// When the script has marked the whole page inert, the status carries a +/// NUL-separated `sealed-page` marker. Ugly, and deliberate: widening the +/// return type would touch every caller for one boolean, and the marker +/// cannot occur in a server reply. +/// /// **This path did not exist before CB-WP-0024 T01, and that is why the /// defect it tests survived.** The fetch stub used to return a /// `then`-chain that never called its callbacks, so nothing the script @@ -259,6 +273,14 @@ pub fn gesture_with_reply( let status: String = ctx .eval_as("__statusText()") .map_err(|e| format!("status: {e}"))?; + let sealed: bool = ctx + .eval_as("__bodySealed()") + .map_err(|e| format!("body sealed: {e}"))?; + let status = if sealed { + format!("{status}\u{0}sealed-page") + } else { + status + }; let keys = if keys.is_empty() { Vec::new() } else { diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index 76db89f..5941d62 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -1326,6 +1326,28 @@ mod ending_page { status.contains("session has ended"), "the page must say what happened: {status:?}" ); + // The WHOLE page goes inert, not only the controls. A greyed + // button beside a full-colour table reads as a live game with one + // broken control. + assert!( + status.contains("sealed-page"), + "the page itself was not marked ended: {status:?}" + ); + } + + /// The controls sit **below** the log: you read what happened, then + /// decide what to do next. + #[test] + fn the_controls_come_after_the_log() { + let html = page(); + let log = html.find("

log

").expect("a log section"); + let again = html + .find("data-drop=\"again\"") + .expect("a play-again control"); + assert!( + again > log, + "the controls are above the log; the reader decides before reading" + ); } /// **A thing you click must not look like a thing you drag.** @@ -1470,6 +1492,12 @@ mod ending_page { live.contains(&"again".to_string()) && live.contains(&"done".to_string()), "an 'ok' reply must not seal the page: {live:?}" ); + let (_, status) = + jsrun::gesture_with_reply(&html, "again", "again", "ok: dealing").expect("run"); + assert!( + !status.contains("sealed-page"), + "a dealing reply greyed out a page that is about to be reused" + ); } } diff --git a/trials/2026-08-06-2230.md b/trials/2026-08-06-2230.md new file mode 100644 index 0000000..834def4 --- /dev/null +++ b/trials/2026-08-06-2230.md @@ -0,0 +1,17 @@ +# Trial log + +What the player said while playing, bound to the position they said it at +(CB-WP-0027, ADR-0014). The recording beside this file is the position; +`state_hash` is what reaches it. + +**These are raw notes and they stay here.** Nothing in this file travels to +`ground-game` (ADR-0014 D4) — a note reaches them only by being promoted to a +register finding, by a human, with the wording chosen then. + + + +| n | round | step | state_hash | comment | +|---|---|---|---|---| +| 1 | 5 | Reveal | bb9de8f0bfe5 | RespondToSupport is unclear as i cant see whos support i accept. | + + diff --git a/trials/2026-08-06-2230.yaml b/trials/2026-08-06-2230.yaml new file mode 100644 index 0000000..195eeda --- /dev/null +++ b/trials/2026-08-06-2230.yaml @@ -0,0 +1,141 @@ +scenario: ground/cb-play-session +description: recorded by cb-play (CB-WP-0008 T02) +covers: [] +provisional: false +provisional_owner: '' +provisional_raised: '' +ruled: '' +ruled_by: '' +ruled_note: '' +encodes_u_item: '' +seed: 2 +setup: + players: 3 + preset: standard-3p + patch: {} +commands: +- actor: P1 + cmd: select_action + args: + action: SOLVE + problem: 1 +- actor: P2 + cmd: select_action + args: + action: SOLVE + problem: 1 +- actor: P3 + cmd: select_action + args: + action: SOLVE + problem: 1 +- actor: SYSTEM + cmd: reveal + args: {} +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +- actor: P1 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: P2 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: P3 + cmd: select_action + args: + action: INVESTIGATE + problem: 2 +- actor: SYSTEM + cmd: reveal + args: {} +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +- actor: P1 + cmd: select_action + args: + action: INVESTIGATE + problem: 3 +- actor: P2 + cmd: select_action + args: + action: SOLVE + problem: 2 +- actor: P3 + cmd: select_action + args: + action: SOLVE + problem: 2 +- actor: SYSTEM + cmd: reveal + args: {} +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +- actor: P1 + cmd: select_action + args: + action: INVESTIGATE + problem: 4 +- actor: P2 + cmd: select_action + args: + action: INVESTIGATE + problem: 4 +- actor: P3 + cmd: select_action + args: + action: INVESTIGATE + problem: 4 +- actor: SYSTEM + cmd: reveal + args: {} +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +- actor: P1 + cmd: select_action + args: + action: SOLVE + problem: 3 +- actor: P2 + cmd: select_action + args: + action: SOLVE + problem: 4 +- actor: P3 + cmd: select_action + args: + action: SOLVE + problem: 4 +- actor: SYSTEM + cmd: reveal + args: {} +- actor: SYSTEM + cmd: resolve + args: {} +- actor: SYSTEM + cmd: end_round + args: {} +expect: + events: [] + state: {} + rejects: [] + state_hash: 12e812982e92dde5e91147dfd67befb8552831c30e3053cfc5cdc5133d025d57