clay-borg/crates/cb-render-html/src/doc.rs

2187 lines
86 KiB
Rust
Raw Normal View History

CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
//! The emitted document: HTML shell, inline SVG table, inline JavaScript.
//!
//! ADR-0007 Decision 1. Marginal AM-4a cost zero — this is string
//! formatting, and the browser draws it.
//!
//! ## What the JavaScript is allowed to be
//!
//! ADR-0007 Decision 5 bars it from constructing commands. [`SCRIPT`] is
//! therefore the whole of it, it is a constant, and it does one thing:
//! record which element the pointer went down on, which it came up on, and
//! POST the pair. It contains no game vocabulary — no action names, no
//! seats, no rules. If a rule ever needs to appear in it, the decision is
//! wrong and ADR-0007 says to revisit it rather than widen the control.
use std::fmt::Write as _;
use cb_kernel::PlayerId;
use games_ground::view::{GroundView, PlayerView, ProblemView, SelectionView};
use crate::input::action_id;
/// Escape for text and attribute contexts alike.
///
/// Everything interpolated into the document goes through this. Card
/// suits and seat numbers cannot currently carry a `<`, but "cannot
/// currently" is how injection bugs are written.
pub fn esc(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&amp;"),
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'"' => out.push_str("&quot;"),
'\'' => out.push_str("&#39;"),
_ => out.push(c),
}
}
out
}
fn seat_name(p: PlayerId) -> String {
format!("P{}", p.0 + 1)
}
fn cards(list: &[games_ground::SolutionCard]) -> String {
if list.is_empty() {
return "none".to_string();
}
list.iter()
.map(|c| format!("{:?}", c.suit))
.collect::<Vec<_>>()
.join(" ")
}
/// The only JavaScript in the project. See the module docs.
pub const SCRIPT: &str = r#"
(function () {
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
var down = null, held = null, marked = [], ghost = null, ghostLabel = '';
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
// What is actually under the pointer, not what the browser decided the
// event belongs to. CB-WP-0020 T03: for touch and pen a browser
// implicitly captures the pointer to the pointerdown target, so
// `e.target` on pointerup can be the element you STARTED on wherever
// you release. That makes a drop look like a drop-on-itself, and the
// "nothing droppable" message unreachable. elementFromPoint is correct
// under both behaviours.
function under(e) {
if (document.elementFromPoint && e.clientX !== undefined) {
return document.elementFromPoint(e.clientX, e.clientY) || e.target;
}
return e.target;
}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
function node(e) {
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
var n = under(e);
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
while (n && !(n.getAttribute && n.getAttribute('data-drop'))) { n = n.parentNode; }
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
return n;
}
function key(n) { return n ? n.getAttribute('data-drop') : null; }
function status(t) { document.getElementById('cb-status').textContent = t; }
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>
2026-08-05 17:23:09 +02:00
// The session has ended server-side, so nothing on this page can work
// any more -- and nothing on it may look like it can (CB-WP-0024 T01).
//
// The old page kept a full table and a live `play again` control after
// the server had stopped listening, because the script acted only on
// 'ok'. A control that cannot work must stop being a control, so the
// `data-drop` attribute is REMOVED rather than styled: that is the
// attribute the script's own walk reads, so the element becomes
// undroppable by the same rule that made it droppable.
//
// No game vocabulary here (ADR-0007 D5). 'closed' is a server
// lifecycle word, exactly like the 'ok' branch below it.
var sealed = false;
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>
2026-08-05 17:23:09 +02:00
function seal() {
sealed = true;
// 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');
}
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>
2026-08-05 17:23:09 +02:00
var all = document.querySelectorAll('[data-drop]');
for (var i = 0; i < all.length; i++) {
all[i].classList.add('sealed');
// removeAttribute, NOT setAttribute(_, null): the latter writes the
// literal string "null" in a real browser, which is truthy, so the
// control would stay droppable while the test stub said otherwise.
all[i].removeAttribute('data-drop');
}
}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
// ADR-0010 D1: the destinations come from `data-targets`, which Rust
// wrote. This matches on them. It does not compute, infer, filter or
// default one -- a script that pattern-matched ids to guess what is
// legal would be forbidden even though the visible result is identical.
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
// ADR-0010 D1 again: `data-descs` is written by Rust, in step with
// `data-targets`. The script pairs them by index and renders one. It
// does not compose a description from an id.
function describeOf(held, key) {
if (!held) { return null; }
var t = held.getAttribute('data-targets');
var d = held.getAttribute('data-descs');
if (!t || !d) { return null; }
var i = t.split(' ').indexOf(key);
var all = d.split('|');
return i >= 0 && i < all.length ? all[i] : null;
}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
function mark(n) {
var spec = n.getAttribute('data-targets');
if (!spec) { return; }
var want = spec.split(' ');
var all = document.querySelectorAll('[data-drop]');
for (var i = 0; i < all.length; i++) {
if (want.indexOf(all[i].getAttribute('data-drop')) >= 0) {
all[i].classList.add('dropok');
marked.push(all[i]);
}
}
}
function clear() {
for (var i = 0; i < marked.length; i++) { marked[i].classList.remove('dropok'); }
marked = [];
if (held) { held.classList.remove('held'); held = null; }
if (ghost && ghost.parentNode) { ghost.parentNode.removeChild(ghost); }
ghost = null;
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
ghostLabel = '';
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
down = null;
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
document.addEventListener('pointerdown', function (e) {
clear();
var n = node(e);
down = key(n);
if (!n || !down) { return; }
held = n;
n.classList.add('held');
mark(n);
if (n.getAttribute('data-targets')) {
ghost = document.createElement('div');
ghost.id = 'cb-ghost';
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
// Keep the label as markup, so the explanation can be appended
// rather than replacing it (CB-WP-0020 T02). The first version set
// textContent, which collapsed the card's line break and then got
// overwritten by the explanation -- losing the only sign of what
// was being carried, exactly when it was needed.
ghostLabel = (n.getAttribute('data-drop') || '').replace('action-', '');
ghost.innerHTML = '<b>' + ghostLabel + '</b>';
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
ghost.style.left = e.clientX + 'px';
ghost.style.top = e.clientY + 'px';
document.body.appendChild(ghost);
}
});
document.addEventListener('pointermove', function (e) {
if (!ghost) { return; }
ghost.style.left = e.clientX + 'px';
ghost.style.top = e.clientY + 'px';
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
// The explanation, beside the pointer and therefore beside the
// target it is over (CB-WP-0018 T03).
var over = describeOf(held, key(node(e)));
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
ghost.innerHTML = '<b>' + ghostLabel + '</b>'
+ (over ? '<span class="why">' + over + '</span>' : '');
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
ghost.className = over ? 'over' : '';
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
});
document.addEventListener('pointercancel', clear);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
document.addEventListener('pointerup', function (e) {
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
var up = key(node(e));
var grabbed = down;
clear();
if (!grabbed || !up) {
// Refusing is right; refusing SILENTLY is what let a broken drop
// target survive a human sitting in front of it (CB-WP-0016).
status(grabbed ? 'took ' + grabbed + ', let go over nothing droppable'
: 'nothing droppable under the pointer');
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
return;
}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
var body = 'down=' + encodeURIComponent(grabbed) + '&up=' + encodeURIComponent(up);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
fetch(window.CB_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
}).then(function (r) { return r.text(); }).then(function (t) {
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
status(t);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
if (t.indexOf('ok') === 0) { window.location.reload(); }
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>
2026-08-05 17:23:09 +02:00
else if (t.indexOf('closed') === 0) { seal(); }
}).catch(gone);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
});
// CB-WP-0035. The session ended without this page being told.
//
// There was no rejection handler at all, so when the server had exited
// the promise rejected and the chain simply never ran -- not even the
// status line moved. `play again` looked like a dead button, and the
// linger timeout was invisible. A request that cannot be answered is
// information, and it was being thrown away.
function gone() {
status('the session is no longer available \u2014 the game server has '
+ 'stopped. Nothing on this page can act; it is a record now.');
seal();
}
// And notice it WITHOUT being clicked, which is what the timeout needs:
// a player who walks away comes back to a sealed page rather than to a
// live-looking table that answers nothing.
if (window.CB_ALIVE) {
setInterval(function () {
if (sealed) { return; }
fetch(window.CB_ALIVE).then(function (r) {
if (!r.ok) { gone(); }
}).catch(gone);
}, 5000);
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
})();
"#;
const STYLE: &str = "
body{font:14px/1.5 ui-monospace,monospace;margin:1.5rem;background:#12141a;color:#dde}
h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf}
.row{display:flex;flex-wrap:wrap;gap:.5rem;align-items:flex-start}
.card{border:1px solid #445;border-radius:6px;padding:.5rem .7rem;background:#1b1e26}
.card[data-viewer=true]{border-color:#9cf}
.act{cursor:grab;user-select:none;background:#243;border-color:#5a7}
.btn{cursor:pointer;background:#332a3a;border-color:#a7d}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
/* CB-WP-0017. Interactive and inert must not look identical: `.pick` is
the resting affordance on anything that can be picked up. ADR-0010 D5
claims no evidence that this READS as pickable -- that is a human
check, and stage 1 is open on it. */
.pick{cursor:grab;user-select:none;box-shadow:0 2px 0 #0006,0 0 0 1px #5a7a inset;
transition:transform .08s,box-shadow .08s}
.pick:hover{box-shadow:0 3px 8px #000a,0 0 0 1px #7ca inset;transform:translateY(-1px)}
fix: a click target wearing a drag affordance made the controls look dead Tier S (a fix inside a boundary; chaos d8=7 from the previous roll stands for this continuation). Two observations from play that are ONE defect. `play again`, `end session`, `pass` and the move buttons carried `.pick`, which is cursor:grab. The stylesheet has .btn{cursor:pointer} BEFORE .pick{cursor:grab}, so grab won. A GRAB CURSOR INVITES A DRAG. A drag released over nothing posts nothing, so the player picked up the button, let go, and the page did nothing. It looked dead because the affordance told them to do the one thing that does not work. Reported as two separate things -- "the button shows a hand to pick up that it probably shouldn't" and "I can't start another game or stop the server" -- and the first causes the second. The click path itself was never broken: driving again->again and done->done through the JS harness posts correctly. The logic was fine and the invitation was wrong. Click targets now carry `.tap` -- pointer cursor, same press affordance. This extends CB-WP-0017's rule (interactive and inert must not look identical) to: click and drag must not look identical either. The test asserts both directions, because checking only that buttons lost `.pick` would pass for a page with no affordances at all. Registered F20 (applied) and F21. F21 IS THE ONE I COULD NOT REPRODUCE: dragging did not work until after the first note was saved. Ruled out the plausible mechanisms -- the gesture logic posts correctly against the served page, the drag ghost carries pointer-events:none so it cannot intercept the drop, and the markup is identical before and after since the 303 re-renders the same page from the same state. Remaining candidates are a <details> toggle shifting layout mid-drag, a first-load timing difference, or browser-level pointer capture. Reproducing it needs a browser, which no test here has -- the same gap F19 named. Recorded as unreproduced rather than given a speculative fix. And the fourth observation is confirmation, not a bug: "drawing my cards from the deck is not implemented, I did not need to do that" is exactly what CB-WP-0028 T04 determined and deliberately did not build. It is the first evidence that importing the card text closed the comprehension gap that produced the earlier click-the-deck request. make all: exit 0. 62 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:25:06 +02:00
/* CB-WP-0017's principle, one step further: a thing you CLICK and a thing
you DRAG must not look identical either. `.pick` promises carry-me-
somewhere; these promise press-me. They wore `.pick` and therefore a
grab cursor, which invited a drag -- and a drag released over nothing
posts nothing, so the button appeared dead. Reported both as the button
showing a hand it should not, AND as being unable to start a new game. */
.tap{cursor:pointer;user-select:none;box-shadow:0 2px 0 #0006,0 0 0 1px #a7d6 inset;
transition:transform .08s,box-shadow .08s}
.tap:hover{box-shadow:0 3px 8px #000a,0 0 0 1px #a7d inset;transform:translateY(-1px)}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
/* A legal destination for the thing currently held -- and ONLY for that
thing. The set is written by Rust into data-targets; the script matches
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
it and never derives it (ADR-0010 D1).
CB-WP-0020 T01, at the maintainer's instruction: change the EXISTING
border, do not draw a new box. `outline` + `outline-offset` drew a
second rectangle outside the element, which an SVG viewport clips (the
reported missing top and left edges) and which made a seat's highlight
the size of a whole card. Restyling the border in place cannot move
anything, because the border is already in the layout. */
.dropok{border-style:dashed;border-color:#9cf;background:#1d2a33}
.dropok circle,.dropok rect{stroke:#9cf;stroke-dasharray:5 3}
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
.dropok text{fill:#cfe}
/* The ghost that follows the pointer, so a drag is not invisible. */
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
#cb-ghost.over{background:#1d3347;border-color:#9cf;color:#cfe}
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
/* The thing in your hand. Deliberately NOT card-shaped: it used to be a
textContent copy of the card, so the line break collapsed and it read
as a second card sitting next to the first (CB-WP-0020 T02). */
#cb-ghost{position:fixed;pointer-events:none;z-index:9;padding:.25rem .55rem;
border-radius:999px;background:#2b4a3a;border:1px solid #7ca;
color:#dfe;font:12px ui-monospace,monospace;
box-shadow:0 6px 16px #000b;transform:translate(-50%,-160%);
white-space:nowrap}
/* The explanation is ADDITIONAL, never a replacement: losing the label is
losing the only sign of what you are carrying. */
#cb-ghost b{color:#cfe}
#cb-ghost .why{color:#9cf;margin-left:.4rem}
/* What you picked up, left visibly behind so there are not two cards. */
.held{cursor:grabbing;opacity:.35;border-style:dashed}
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>
2026-08-05 17:23:09 +02:00
/* CB-WP-0024 T01: a control the server can no longer serve. The script
removes its `data-drop` so it is genuinely inert; this is only how that
reads. `pointer-events:none` is belt and braces, not the mechanism --
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 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
/* 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.
The single-column fallback is deliberate rather than incidental: the
page was responsive by accident before this. */
.cb-cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,24rem);
gap:1.2rem;align-items:start}
.cb-game{min-width:0}
.cb-meta{min-width:0;border-left:1px solid #2a3140;padding-left:1.1rem}
#cb-note{display:flex;flex-direction:column;gap:.5rem}
#cb-note textarea{width:100%;box-sizing:border-box;font:inherit;color:#dde;
background:#12141a;border:1px solid #445;border-radius:5px;padding:.5rem}
#cb-note button{align-self:flex-start;font:inherit;cursor:pointer;color:#dde;
background:#332a3a;border:1px solid #a7d;border-radius:5px;padding:.35rem .8rem}
.note{border-left:2px solid #a7d;padding-left:.6rem;margin:.4rem 0;white-space:pre-wrap}
@media (max-width:64rem){
.cb-cols{grid-template-columns:minmax(0,1fr)}
.cb-meta{border-left:none;border-top:1px solid #2a3140;padding-left:0;padding-top:1rem}
}
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
/* CB-WP-0024 T03: the card a seat played, in that seat's area. */
.played{display:block;margin:.3rem 0}
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
/* CB-WP-0028 T02: the card's own words. The tagline reads as the card's
voice, not as engine chrome. */
.tag{display:block;color:#9cb;font-style:italic;margin:.15rem 0 .3rem}
details summary{cursor:pointer;color:#89a;font-size:.9em;margin-top:.3rem}
details p,details{margin:.2rem 0}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
.k{color:#89a}
.nil{color:#c88}
.eff{color:#8c9}
#cb-log{max-height:16rem;overflow-y:auto;font-size:13px}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
#cb-status{margin-top:1rem;color:#fc9;min-height:1.2em}
svg{background:#1b1e26;border:1px solid #445;border-radius:6px}
";
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
/// Render drop keys as something a player can read.
///
/// The keys are what the page posts; these are what it shows. Both come
/// from the same list, so they cannot disagree about which moves exist.
fn target_names(targets: &[String]) -> String {
let mut out: Vec<String> = Vec::new();
for t in targets {
out.push(match t.split_once('-') {
Some(("seat", n)) => n
.parse::<u8>()
.map(|n| seat_name(PlayerId(n)))
.unwrap_or_else(|_| t.clone()),
Some(("problem", n)) => format!("problem {n}"),
_ if t == "table" => "the table".to_string(),
_ => t.clone(),
});
}
out.join(", ")
}
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
/// The edition's title for a Problem, by `hidden_priority` (ADR-0015 D1).
///
/// These columns were in the vendored file from the start and thrown away
/// at parse time — the page showed `Repair 2` for a card that reads
/// *"Missed Deadline"*.
fn problem_title(priority: u32) -> Option<String> {
games_ground::edition::problem_texts("SCN_01")
.ok()?
.into_iter()
.find(|t| u32::from(t.priority) == priority)
.map(|t| t.title)
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
fn problem_svg(out: &mut String, priority: u32, p: &ProblemView, x: i32) {
let (label, sub, fill) = match p {
ProblemView::FaceDown => ("face down".to_string(), String::new(), "#2a2f3a"),
ProblemView::FaceUp {
suit,
value,
denied,
claimed_by,
protected_this_round,
} => {
let mut sub = String::new();
if *denied {
sub.push_str("denied ");
}
if *protected_this_round {
sub.push_str("protected ");
}
if let Some(c) = claimed_by {
let _ = write!(sub, "claimed by {}", seat_name(*c));
}
(
format!("{suit:?} {value}"),
sub.trim_end().to_string(),
"#26303a",
)
}
};
let _ = write!(
out,
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
"<g data-drop=\"problem-{priority}\"><rect x=\"{x}\" y=\"10\" width=\"120\" height=\"78\" rx=\"8\" \
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
fill=\"{fill}\" stroke=\"#5a6b7a\"/>\
<circle cx=\"{bx}\" cy=\"24\" r=\"11\" fill=\"#1b1e26\" stroke=\"#5a6b7a\"/>\
<text x=\"{bx}\" y=\"29\" fill=\"#9cf\" font-size=\"13\" text-anchor=\"middle\">\
{priority}</text>\
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
<text x=\"{tx}\" y=\"36\" fill=\"#dde\" font-size=\"13\">{label}</text>\
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
<text x=\"{tx}\" y=\"56\" fill=\"#89a\" font-size=\"11\">{name}</text>\
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
<text x=\"{tx}\" y=\"74\" fill=\"#fc9\" font-size=\"11\">{sub}</text></g>",
x = x,
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
// The card's own name where a priority number used to be. Falls
// back to the number rather than blanking: a missing title must
// not remove the only identifier the player had.
name = esc(&problem_title(priority).unwrap_or_else(|| format!("priority {priority}"))),
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
tx = x + 10,
// CB-WP-0045: the number, on the card.
//
// Every move label says "Problem 7" and nothing on the table said
// which card that was. In the old row the position was a hint;
// once Problems are placed by scope the row is gone and the hint
// with it. The number was only ever a FALLBACK for a missing
// title — visible exactly when it was least useful.
bx = x + 104,
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
label = esc(&label),
sub = esc(&sub),
);
}
/// The relationship graph — the one element every Rust 2D toolkit would
/// have left us to hand-roll, and the reason SVG earns its place here
/// rather than merely fitting the budget (CB-RES-0006 §5).
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>
2026-08-05 17:23:09 +02:00
/// One pile, drawn as a small stack of offset cards with its count on top.
///
/// `depth` is how many card-backs to suggest, not the count — a pile of 17
/// is not seventeen rectangles. The **number** is the truth; the stack is
/// how you tell at a glance that there is a pile there at all.
fn pile_svg(out: &mut String, x: i32, label: &str, count: usize, face: &str, note: &str) {
let depth = match count {
0 => 0,
1..=3 => 1,
4..=9 => 2,
_ => 3,
};
// The title carries the count in words. It is what a screen reader
// announces, and it is the stable thing a coverage probe can match --
// the on-canvas number is a bare numeral that could be anything.
let _ = write!(
out,
"<g><title>{} pile: {count} remaining</title>",
esc(label),
);
if depth == 0 {
// An EMPTY pile is drawn, not omitted. A missing slot reads as
// "this game has no discard", which is a different statement from
// "the discard is empty" (CB-WP-0024 T02).
let _ = write!(
out,
"<rect x=\"{x}\" y=\"14\" width=\"76\" height=\"64\" rx=\"7\" fill=\"none\" \
stroke=\"#3a4350\" stroke-dasharray=\"4 3\"/>",
);
}
for d in (0..depth).rev() {
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
let dx = x + d * 3;
let dy = 14 - d * 3;
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>
2026-08-05 17:23:09 +02:00
let _ = write!(
out,
"<rect x=\"{dx}\" y=\"{dy}\" width=\"76\" height=\"64\" rx=\"7\" fill=\"{face}\" \
stroke=\"#5a6b7a\"/>",
);
}
let _ = write!(
out,
"<text x=\"{tx}\" y=\"46\" fill=\"#dde\" font-size=\"18\" text-anchor=\"middle\">{count}</text>\
<text x=\"{tx}\" y=\"64\" fill=\"#89a\" font-size=\"10\" text-anchor=\"middle\">{label}</text>\
<text x=\"{tx}\" y=\"96\" fill=\"#fc9\" font-size=\"10\" text-anchor=\"middle\">{note}</text></g>",
tx = x + 38,
count = count,
label = esc(label),
note = esc(note),
);
}
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
fn piles_body(out: &mut String, view: &GroundView) {
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>
2026-08-05 17:23:09 +02:00
let deck = view.solution_deck_len;
let discard = view.solution_discard.len();
let will_reshuffle = deck == 0 && discard > 0;
pile_svg(
out,
10,
"draw",
deck,
"#26303a",
if will_reshuffle { "empty" } else { "" },
);
pile_svg(
out,
120,
"discard",
discard,
"#2a2f3a",
if will_reshuffle {
"shuffles in on next draw"
} else {
""
},
);
if will_reshuffle {
out.push_str(
"<path d=\"M100 46 q10 -14 20 0\" fill=\"none\" stroke=\"#fc9\" stroke-width=\"2\"/>\
<path d=\"M118 40 l4 6 -7 1z\" fill=\"#fc9\"/>",
);
}
}
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
/// A marker on a track, drawn at `(x, y)` with `n` stops.
///
/// **The track is the point** (ADR-0016 D4). `stress 5` is a fact you
/// read; a marker at the end of a 0-5 track is a fact you see coming --
/// and DARVO triggers at Stress 5, so "one more Attack and I trigger" is
/// the most useful thing the page can show.
fn track_svg(out: &mut String, x: f64, y: f64, stops: usize, at: usize, hot: bool, label: &str) {
let step = 11.0;
let _ = write!(out, "<g><title>{}</title>", esc(label));
for i in 0..stops {
let cx = x + (i as f64) * step;
let here = i == at;
let _ = write!(
out,
"<circle cx=\"{cx:.0}\" cy=\"{y:.0}\" r=\"{r}\" fill=\"{fill}\" \
stroke=\"{stroke}\" stroke-width=\"1\"/>",
r = if here { 4.5 } else { 2.5 },
fill = if here {
if hot {
"#e77"
} else {
"#9cf"
}
} else {
"#2a3140"
},
stroke = if here { "#fff8" } else { "#3a4350" },
);
}
out.push_str("</g>");
}
/// The tokens at one seat, as objects rather than numbers.
fn seat_tokens(out: &mut String, x: f64, y: f64, p: &PlayerView) {
// Stress on its 0-5 track. Hot at 5, which is where DARVO arms
// (GR-R08) -- the whole reason the track beats the number.
track_svg(
out,
x - 27.0,
y + 26.0,
6,
usize::from(p.stress).min(5),
p.stress >= 5,
&format!("Stress {} of 5", p.stress),
);
// The DARVO pawn on OFF/DENY/ATTACK/REVERSE.
let stage = match p.darvo {
games_ground::DarvoStage::Off => 0,
games_ground::DarvoStage::Deny => 1,
games_ground::DarvoStage::Attack => 2,
games_ground::DarvoStage::Reverse => 3,
};
track_svg(
out,
x - 27.0,
y + 38.0,
4,
stage,
stage > 0,
&format!("DARVO {:?}", p.darvo),
);
// Counted discs: Freedom (double-sided), Protection, Blame.
let mut dx = x - 28.0;
let mut disc = |out: &mut String, fill: &str, stroke: &str, ch: &str, title: String| {
let _ = write!(
out,
"<g><title>{t}</title><circle cx=\"{dx:.0}\" cy=\"{cy:.0}\" r=\"7\" \
fill=\"{fill}\" stroke=\"{stroke}\"/>\
<text x=\"{dx:.0}\" y=\"{ty:.0}\" fill=\"#dfe\" font-size=\"8\" \
text-anchor=\"middle\">{ch}</text></g>",
t = esc(&title),
cy = y + 54.0,
ty = y + 57.0,
);
dx += 17.0;
};
disc(
out,
if p.freedom_ready {
"#2b4a3a"
} else {
"#2a2f3a"
},
if p.freedom_ready { "#7ca" } else { "#4a5260" },
if p.freedom_ready { "R" } else { "\u{2013}" },
format!(
"Freedom {}",
if p.freedom_ready { "READY" } else { "spent" }
),
);
for i in 0..p.protection {
disc(out, "#2a3a4a", "#7ac", "P", format!("Protection {}", i + 1));
}
for b in &p.blame_from {
disc(
out,
"#3a2a2a",
"#c88",
"B",
format!("Blame from {}", seat_name(*b)),
);
}
}
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
/// The table, seen from above (CB-WP-0028 T03).
///
/// **One diagram, not three.** The seats were already placed on a circle
/// for the relationship graph, while the Problems were a row above it and
/// the piles a separate picture below — three views of one table, and the
/// player had to assemble them. Now: seats around the edge, the Problems
/// and the two stacks in the middle, each seat's played card in front of
/// it, relations drawn between seats.
///
/// The existing `problem_svg` and `pile_svg` are reused rather than
/// reimplemented — they carry the tokens the coverage gate probes for,
/// and drawing the same thing twice is how the two drift.
fn table_svg(view: &GroundView) -> String {
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let n = view.players.len().max(1);
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
// Compact on purpose. The first version was 760x620, which pushed the
// action cards a full screen below the Problems -- and you cannot drag
// between two things that are never on screen together, which made the
// game unplayable. Height is the constraint, not width.
let (cx, cy) = (380.0f64, 215.0f64);
let (rx, ry) = (200.0f64, 118.0f64);
// Seats sit OUTSIDE the table, as people do. The first version put
// them at 0.83 of the ellipse and they sat on it.
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
// Seats sit outside the table with room below each for its tokens.
let (sx, sy) = (rx + 112.0, ry + 82.0);
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let pos: Vec<(PlayerId, f64, f64)> = view
.players
.keys()
.enumerate()
.map(|(i, p)| {
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
// Start at the bottom: the viewer sits nearest the reader.
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
let a = std::f64::consts::TAU * (i as f64) / (n as f64) + std::f64::consts::FRAC_PI_2;
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
(*p, cx + sx * a.cos(), cy + sy * a.sin())
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
})
.collect();
let find = |p: PlayerId| {
pos.iter()
.find(|(q, _, _)| *q == p)
.map(|(_, x, y)| (*x, *y))
};
let mut s = String::from(
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
"<svg viewBox=\"0 0 760 470\" width=\"100%\" style=\"max-width:760px\" \
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
role=\"img\" aria-label=\"the table, seen from above\">",
);
// THE TABLE IS THE DROP ZONE. `table` was an abstract id with no
// picture; the player looked for somewhere to drop GROUND and there
// was nowhere. Now the surface you can see is the surface you drop on.
let _ = write!(
s,
"<g data-drop=\"table\"><ellipse cx=\"{cx}\" cy=\"{cy}\" rx=\"{rx}\" ry=\"{ry}\" \
fill=\"#171b23\" stroke=\"#2a3140\" stroke-width=\"2\"/>\
<text x=\"{cx}\" y=\"{ty}\" fill=\"#556\" font-size=\"10\" \
text-anchor=\"middle\">the table</text></g>",
ty = cy + ry - 8.0,
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
);
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
for (pair, rel) in &view.relations {
if let (Some((x1, y1)), Some((x2, y2))) = (find(pair.0), find(pair.1)) {
let colour = match rel {
games_ground::Relation::Bond => "#5c9",
games_ground::Relation::Rivalry => "#c66",
};
let _ = write!(
s,
"<line x1=\"{x1:.0}\" y1=\"{y1:.0}\" x2=\"{x2:.0}\" y2=\"{y2:.0}\" \
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
stroke=\"{colour}\" stroke-width=\"2\" stroke-opacity=\"0.7\"/>\
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
<text x=\"{mx:.0}\" y=\"{my:.0}\" fill=\"{colour}\" font-size=\"10\" \
text-anchor=\"middle\">{rel:?}</text>",
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
mx = (x1 + x2) / 2.0,
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
my = (y1 + y2) / 2.0 - 4.0,
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
);
}
}
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
CB-WP-0043: a Problem sits where its Stress lands The picture had become a lie. A row across the middle says "these are the table's" — true under the baseline, false under H2, where three of five cards fall on particular seats. Global goes to the middle, personal beside its owner, and bond on the mean midpoint of the owner's Bond edges. A bond Problem whose owner has no Bond is drawn as personal, because that is the rule — bond_network falls back to the owner at degree 0 — and the picture must agree with the arithmetic. The baseline page is untouched: the scoped layout is taken only when a non-global scope exists, so every prior look at the baseline still holds. The scope reaches the view as a separate marker rather than a field on ProblemView, because the delta says "place owner marker on the card" — a token beside a card — and it is public while the card is face down, which a ProblemView variant could not express. The coverage probe forced a real improvement. It demanded a text token for the new fields and a POSITION is not a token — which is the probe being right: position alone is invisible to text_of and to a screen reader, and illegible when two anchors coincide. So each scoped Problem now says whose it is: everyone's, P1's alone, P2's Bond network, and "P1's alone — no Bond to share it" at degree 0. The fixture gained a third Problem. With two, one of the three placement rules was unexercised and the probe unsatisfiable — a fixture that cannot reach a branch is how a rule ships untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:30:14 +02:00
// **Where a Problem sits says who it falls on** (CB-WP-0043).
//
// H2 scopes End-of-Round Stress: `global` hits everyone, `personal`
// its owner, `bond` the owner's Bond network. A row across the middle
// says "these are the table's" — true under the baseline, and a lie
// under H2, where three of five cards belong to particular seats.
//
// **This shows mechanism; it does not invent any.** The scope and
// owner come off the state; only the position is ours
// (`specs/Ornamentation.md`).
let scoped = view.problem_markers.values().any(|m| {
m.scope
.is_some_and(|x| x != games_ground::edition::StressScope::Global)
});
if !scoped {
// Baseline, unchanged: a row across the middle, because under
// the baseline every Problem really is the table's.
let count = view.problems.len().max(1) as f64;
let scale = (2.0 * rx * 0.92 / (count * 130.0)).min(0.82);
let _ = write!(
s,
"<g transform=\"translate({x:.0},{y:.0}) scale({scale:.3})\">",
x = cx - count * 130.0 * scale / 2.0,
y = cy - ry + 24.0,
);
for (i, (priority, p)) in view.problems.iter().enumerate() {
problem_svg(&mut s, *priority, p, (i as i32) * 130);
}
s.push_str("</g>");
} else {
scoped_problems(&mut s, view, &pos, cx, cy, ry);
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
}
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
let _ = write!(
s,
"<g transform=\"translate({x:.0},{y:.0}) scale(0.62)\">",
x = cx - 300.0 * 0.62 / 2.0,
y = cy + 8.0,
);
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
piles_body(&mut s, view);
s.push_str("</g>");
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
for (p, x, y) in &pos {
let is_viewer = view.viewer == Some(*p);
let focus = view
.focus
.get(p)
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
.map(|f| format!(" \u{2192}{}", seat_name(*f)))
.unwrap_or_default();
let pv = &view.players[p];
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
// The played card sits between the seat and the table.
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
if let Some(sel) = view.selections.get(p) {
let _ = write!(
s,
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
"<g transform=\"translate({px:.0},{py:.0}) scale(0.55)\">",
px = x + (cx - x) * 0.34 - 23.0,
py = y + (cy - y) * 0.34 - 15.0,
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
);
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
played_svg(&mut s, sel);
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
s.push_str("</g>");
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let _ = write!(
s,
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
"<g data-drop=\"seat-{raw}\">\
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
<circle class=\"seat\" cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
stroke=\"{stroke}\" stroke-width=\"{sw}\"/>\
<text x=\"{x:.0}\" y=\"{t1:.0}\" fill=\"#dde\" font-size=\"12\" \
text-anchor=\"middle\">{name}{you}</text>\
<text x=\"{x:.0}\" y=\"{t2:.0}\" fill=\"#89a\" font-size=\"10\" \
text-anchor=\"middle\">stress {stress}{focus}</text></g>",
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
raw = p.0,
stroke = if is_viewer { "#9cf" } else { "#5a6b7a" },
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
sw = if is_viewer { 3 } else { 2 },
t1 = y - 2.0,
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
t2 = y + 13.0,
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
name = seat_name(*p),
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
you = if is_viewer { " (you)" } else { "" },
stress = pv.stress,
focus = esc(&focus),
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
);
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
// CB-WP-0029 T02: the components, at their seat.
seat_tokens(&mut s, *x, *y, pv);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
CB-WP-0029 T01-T03: components you can count, and a supply that does not bind ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. The link row is the interesting one -- GR-L01's two-slot rule IS the twelve-token supply written twice, which is the shape of a supply needing no separate enforcement. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK WAS WRONG. It compared a seat's own placed Focus against its OWN blame_from -- but that list holds OTHER players' discs, so they are different tokens. It reported 2 conflicts; corrected, it reports 0. Fifth instance of this project's recurring defect, a number computed correctly about the wrong subject, and the first caught before it left the repo rather than by a reviewer. D2: a token is a VIEW, not a type. The aggregate gains no `Token` -- adding one would create a second source of truth for Stress, and the first time they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token would enforce something nobody ruled -- CB-WP-0023's error inverted: SOLVE was OFFERED where it could not act; this would REFUSE where the rules allow. The check ships as a standing control, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:54:20 +02:00
// Lead and Round belong to the table, not to a seat (ADR-0016 D4).
let _ = write!(
s,
"<g><title>Lead marker: {lead}</title>\
<circle cx=\"36\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
<text x=\"36\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
text-anchor=\"middle\">LEAD</text></g>\
<text x=\"52\" y=\"28\" fill=\"#89a\" font-size=\"11\">{lead}</text>\
<g><title>Round marker: round {round} of 5</title>\
<circle cx=\"724\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
<text x=\"724\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
text-anchor=\"middle\">{round}</text></g>\
<text x=\"706\" y=\"28\" fill=\"#89a\" font-size=\"11\" \
text-anchor=\"end\">round</text>",
lead = seat_name(view.lead),
round = view.round,
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
s.push_str("</svg>");
s
}
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
/// A revealed selection, phrased the way the log phrases the command.
///
/// Deliberately the same shape as `event_line`'s `ActionSelected` arm —
/// a second vocabulary for the same fact drifts from the first, which is
/// exactly what `{:?}` was doing here.
fn selection_words(s: &games_ground::Selection) -> String {
// Every field that is set is named. The first draft matched on
// `(target, problem)` and showed only the target when both were
// present — the coverage gate caught it, because a field the document
// never shows is a field a player never sees. The aggregate does not
// currently produce both, but a renderer that silently drops one is
// the omission class this crate exists to guard against.
let mut out = format!("{:?}", s.action);
if let Some(t) = s.target {
let _ = write!(out, " on {}", seat_name(t));
}
if let Some(n) = s.problem {
let _ = write!(out, " for problem {n}");
}
out
}
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
/// The card a seat has played, as a card.
///
/// **The face-down back is a constant.** It takes no argument, because
/// `SelectionView::Hidden` carries nothing and this function must not be
/// able to leak what it does not receive. GR-R02/R04 hide another seat's
/// choice until Reveal, and `view.rs`'s own test asserts the projection
/// obeys that — but a renderer can leak what the model did not, by
/// tinting the back with the suit or shaping it by the action. So there is
/// exactly one back, with no data path into it.
const CARD_BACK: &str = "<svg viewBox=\"0 0 84 56\" width=\"84\" height=\"56\" class=\"played\" \
role=\"img\"><title>face down</title>\
<rect x=\"2\" y=\"2\" width=\"80\" height=\"52\" rx=\"7\" fill=\"#2a2f3a\" stroke=\"#5a6b7a\"/>\
<path d=\"M14 14 h56 M14 28 h56 M14 42 h56\" stroke=\"#3d4756\" stroke-width=\"3\"/></svg>";
fn played_svg(out: &mut String, sel: &SelectionView) {
let s = match sel {
SelectionView::Hidden => {
out.push_str(CARD_BACK);
return;
}
SelectionView::Shown(s) => s,
};
let mut sub = String::new();
if let Some(t) = s.target {
let _ = write!(sub, "\u{2192}{}", seat_name(t));
}
if let Some(n) = s.problem {
if !sub.is_empty() {
sub.push(' ');
}
let _ = write!(sub, "#{n}");
}
let _ = write!(
out,
"<svg viewBox=\"0 0 84 56\" width=\"84\" height=\"56\" class=\"played\" role=\"img\">\
<title>played {label}</title>\
<rect x=\"2\" y=\"2\" width=\"80\" height=\"52\" rx=\"7\" fill=\"#243\" stroke=\"#5a7\"/>\
<text x=\"42\" y=\"27\" fill=\"#dfe\" font-size=\"12\" text-anchor=\"middle\">{label}</text>\
<text x=\"42\" y=\"43\" fill=\"#9cb\" font-size=\"10\" text-anchor=\"middle\">{sub}</text>\
</svg>",
label = esc(&format!("{:?}", s.action)),
sub = esc(&sub),
);
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView) {
let is_viewer = view.viewer == Some(id);
let _ = write!(
out,
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
// CB-WP-0016 T01: the seat card is a drop target. It could not be
// while drop keys were `id`s — the graph node had already taken
// `seat-{n}` and ids must be unique, so the card the instruction
// text points at silently had none.
"<div class=\"card\" data-viewer=\"{is_viewer}\" data-drop=\"seat-{raw}\">\
<b>{name}</b>{you}<br>",
raw = id.0,
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
name = seat_name(id),
you = if is_viewer { " (you)" } else { "" },
);
let _ = write!(
out,
"<span class=\"k\">stress</span> {} <span class=\"k\">protect</span> {} \
<span class=\"k\">darvo</span> {:?}<br>",
p.stress, p.protection, p.darvo
);
let _ = write!(
out,
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
"<span data-drop=\"freedom-{raw}\" class=\"act pick\"><span class=\"k\">freedom</span> \
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
{ready}{lifted}</span><br>",
raw = id.0,
ready = if p.freedom_ready { "READY" } else { "spent" },
lifted = if p.freedom_gate_lifted {
" gate lifted"
} else {
""
},
);
let blame = if p.blame_from.is_empty() {
"none".to_string()
} else {
p.blame_from
.iter()
.map(|b| seat_name(*b))
.collect::<Vec<_>>()
.join(" ")
};
let _ = write!(
out,
"<span class=\"k\">blamed by</span> {}<br>",
esc(&blame)
);
match &p.hand {
Some(h) => {
let _ = write!(
out,
"<span class=\"k\">hand</span> {} ({} cards)<br>",
esc(&cards(h)),
p.hand_size
);
}
None => {
let _ = write!(
out,
"<span class=\"k\">hand</span> {} card(s), hidden<br>",
p.hand_size
);
}
}
if let Some(sel) = view.selections.get(&id) {
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
// The picture, then the words. CB-WP-0024 T03 adds the card; the
// sentence stays, because the log is the record and a player
// reading back through it needs the same vocabulary.
played_svg(out, sel);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let _ = write!(
out,
"<span class=\"k\">selected</span> {}<br>",
match sel {
SelectionView::Hidden => "face down".to_string(),
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
// CB-WP-0020 T04: in words, not `Selection { action:
// Solve, target: None, problem: Some(1) }`. After Reveal
// this is how a player follows what everyone else did,
// and it was the same Debug-on-a-player-surface defect
// the log fixed in CB-WP-0018 and this did not.
SelectionView::Shown(s) => esc(&selection_words(s)),
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
);
}
2026-08-07 17:49:06 +02:00
// CB-WP-0034. These four were `{:?}` on a player surface, which is
// the defect `selection_words` above was written for -- fixed once,
// for one field, with its four neighbours left as they were.
//
// The `support` line is the one the maintainer reported: it read
// `support AcceptBond`, which names the answer and not the person.
// CB-WP-0045: **what this DARVO stage will do**, in the edition's own
// words. `DARVO.csv` carries `mandatory_effect` per stage; we vendored
// it for tripwires and never showed it, so the page said "DARVO
// Reverse" and left the player to guess. Reported as *"the GROUND and
// DARVO logic is far from self explanatory"*.
if p.darvo != games_ground::DarvoStage::Off {
if let Some(text) = darvo_stage_text(p.darvo) {
let _ = write!(
out,
"<details><summary>what {:?} does</summary>{}</details>",
p.darvo,
esc(&text)
);
}
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
if let Some(m) = view.ground_modes.get(&id) {
2026-08-07 17:49:06 +02:00
let _ = write!(
out,
"<span class=\"k\">ground mode</span> {}<br>",
esc(ground_mode_label(m))
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
if let Some(c) = view.ground_choices.get(&id) {
2026-08-07 17:49:06 +02:00
let _ = write!(
out,
"<span class=\"k\">ground choice</span> {}<br>",
esc(&ground_choice_label(c))
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
if let Some(r) = view.support_responses.get(&id) {
2026-08-07 17:49:06 +02:00
let _ = write!(
out,
"<span class=\"k\">support</span> {}<br>",
esc(&support_words(r, supporter_of(view, Some(id))))
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
if let Some(t) = view.darvo_targets.get(&id) {
2026-08-07 17:49:06 +02:00
let _ = write!(
out,
"<span class=\"k\">darvo target</span> {}<br>",
esc(&darvo_target_words(t))
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
out.push_str("</div>");
}
/// Render the whole table as a standalone document.
///
/// `may_pass` adds the one affordance that is not a command: declining to
/// act where the driver allows it. Like every other element it carries an
/// id and nothing else — the page still reports only that the pointer went
/// down and up on `pass`.
///
/// `legal` is the list the aggregate offered; the buttons carry indices
/// into it and nothing else (ADR-0007 control 5). `endpoint` carries the
/// per-process token (control 1) — the page cannot mint one.
pub fn document(
view: &GroundView,
legal: &[games_ground::GroundCommand],
endpoint: &str,
seat: Option<PlayerId>,
may_pass: bool,
) -> String {
2026-08-06 15:32:09 +02:00
document_with_log(
view,
legal,
Endpoints {
command: endpoint,
note: "/note",
alive: "/alive",
2026-08-06 15:32:09 +02:00
},
seat,
may_pass,
Account::of(&[]),
2026-08-06 15:32:09 +02:00
&[],
)
}
/// The table, plus the game log (CB-WP-0018 T02).
2026-08-06 15:32:09 +02:00
/// Where the page posts, both channels (ADR-0014 D1).
///
/// One struct rather than two `&str` parameters because they are one
/// concept — the guarded surface this page may talk to — and because
/// clippy was right that the signature had grown across three passes.
#[derive(Debug, Clone, Copy)]
pub struct Endpoints<'a> {
/// Pointer facts. `resolve` turns these into commands.
pub command: &'a str,
/// Free text. Nothing turns these into commands.
pub note: &'a str,
/// Where the page checks the session is still there (CB-WP-0035).
pub alive: &'a str,
2026-08-06 15:32:09 +02:00
}
pub fn document_with_log(
view: &GroundView,
legal: &[games_ground::GroundCommand],
2026-08-06 15:32:09 +02:00
to: Endpoints<'_>,
seat: Option<PlayerId>,
may_pass: bool,
log: Account<'_>,
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
meta: &[String],
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
) -> String {
let (endpoint, note_to, alive) = (to.command, to.note, to.alive);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let mut s = String::with_capacity(8192);
let _ = write!(
s,
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
<title>GROUND \u{2014} round {round}</title><style>{STYLE}</style></head><body>",
round = view.round
);
let _ = write!(
s,
"<h1>GROUND \u{2014} round {round}, step {step:?}</h1>\
<div><span class=\"k\">lead</span> {lead} \
<span class=\"k\">scoring</span> {mode:?} \
CB-WP-0044: the page says which rules it plays Reported as "I did a session and noted no changes" — correct, and the fault was ours twice. make ground passes no --variant, so the session was baseline, and CB-WP-0043 deliberately leaves the baseline layout untouched because a variant that redraws the baseline invalidates every prior look at it. But the deeper defect is that nothing said so. The page named the round, the step, the Lead, the scoring mode and the viewer, and never which rules it was playing — GroundView did not even carry the variant. There was no way from the screen to tell baseline from H1 from H2. A session that cannot say which rules it is playing cannot report a rules change; the player did the right thing and the instrument had nothing to tell them. Now: variant on GroundView, `rules <id>` in the page header beside the scoring mode, `rules <id>` in the inspector because a replay that cannot say which rules produced it is the same defect in the other tool, and make ground VARIANT=h2 so the capability is reachable. Both coverage probes caught the new field independently — the render crate's and cb-play's — the second time in two passes that they have turned an addition into a legibility requirement instead of letting it be silent state. Verified by fetching the served page rather than by reading the code: make ground VARIANT=h2 prints "rules: h2" and the page carries "rules h2-scoped-problem-stress" with the scope labels; the baseline says "rules ground-darvo-r0" and keeps its row. Still open: nothing explains what a scope DOES, and the trial log header does not record the variant either — the same defect one artifact along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:47:31 +02:00
<span class=\"k\">rules</span> {variant} \
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
<span class=\"k\">viewing as</span> {who}</div>",
round = view.round,
step = view.step,
lead = seat_name(view.lead),
mode = view.mode,
CB-WP-0044: the page says which rules it plays Reported as "I did a session and noted no changes" — correct, and the fault was ours twice. make ground passes no --variant, so the session was baseline, and CB-WP-0043 deliberately leaves the baseline layout untouched because a variant that redraws the baseline invalidates every prior look at it. But the deeper defect is that nothing said so. The page named the round, the step, the Lead, the scoring mode and the viewer, and never which rules it was playing — GroundView did not even carry the variant. There was no way from the screen to tell baseline from H1 from H2. A session that cannot say which rules it is playing cannot report a rules change; the player did the right thing and the instrument had nothing to tell them. Now: variant on GroundView, `rules <id>` in the page header beside the scoring mode, `rules <id>` in the inspector because a replay that cannot say which rules produced it is the same defect in the other tool, and make ground VARIANT=h2 so the capability is reachable. Both coverage probes caught the new field independently — the render crate's and cb-play's — the second time in two passes that they have turned an addition into a legibility requirement instead of letting it be silent state. Verified by fetching the served page rather than by reading the code: make ground VARIANT=h2 prints "rules: h2" and the page carries "rules h2-scoped-problem-stress" with the scope labels; the baseline says "rules ground-darvo-r0" and keeps its row. Still open: nothing explains what a scope DOES, and the trial log header does not record the variant either — the same defect one artifact along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:47:31 +02:00
// CB-WP-0044: the page must say which rules it is playing. It did
// not, so a player who ran the default and was told the layout had
// changed reported "no changes" — correctly, because the baseline
// is deliberately unchanged and nothing on screen said so.
variant = esc(view.variant.id()),
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
who = match view.viewer {
Some(p) => format!("{} (their hand only)", seat_name(p)),
None => "a spectator (no hands)".to_string(),
},
);
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
// CB-WP-0027 T02: the table on the left, everything *about* the table
// on the right. The log moves right because it is commentary on the
// game rather than part of it.
s.push_str("<div class=\"cb-cols\"><div class=\"cb-game\">");
body(&mut s, view);
2026-08-07 17:49:06 +02:00
move_section(&mut s, view, legal, seat, may_pass);
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
s.push_str("</div><div class=\"cb-meta\">");
2026-08-06 15:32:09 +02:00
meta_section(&mut s, meta, note_to);
log_section(&mut s, log);
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
s.push_str("</div></div>");
let _ = write!(
s,
"<div id=\"cb-status\">ready</div>\
<script>window.CB_ENDPOINT={endpoint};window.CB_ALIVE={alive}</script>\
<script>{SCRIPT}</script></body></html>",
endpoint = json_string(endpoint),
alive = json_string(alive),
);
s
}
/// The comment box, shared by the playing page and the ending page.
///
/// **Extracted because the ending page needs the same box** (CB-WP-0031).
/// A second copy would be a second thing to forget: the playing page's
/// form already had the token-carrying action and the works-without-script
/// property, and a hand-written twin on the ending page is how one of them
/// quietly stops posting anywhere useful.
///
/// The `placeholder` differs, and only that — what a player has to say
/// mid-turn and what they have to say having seen the result are not the
/// same prompt.
fn note_form(s: &mut String, note_to: &str, placeholder: &str) {
// A plain form POSTing to /note, so it works with the script disabled.
// The command channel needs JavaScript because a drag is not a form
// submission; a comment is, and making it depend on the script would
// add a failure mode for no gain.
let _ = write!(
s,
"<h2>what are you thinking?</h2>\
<form class=\"card\" method=\"post\" action=\"{note_to}\" id=\"cb-note\">\
<textarea name=\"note\" rows=\"4\" placeholder=\"{placeholder}\"></textarea>\
<button type=\"submit\">note it</button></form>",
note_to = esc(note_to),
placeholder = esc(placeholder),
);
}
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
/// The meta panel's own content: whatever the caller wants a player to see
/// *about* the session rather than about the position.
///
/// Empty is a legitimate state — a first game has no tally and may have no
/// notes — and renders as nothing rather than as an empty heading.
2026-08-06 15:32:09 +02:00
fn meta_section(s: &mut String, meta: &[String], note_to: &str) {
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
// CB-WP-0027 T03: the comment box. Always present — the panel's
// purpose is that a player can say something at any moment, and a box
// that appears only sometimes trains them not to look for it.
//
// A plain form POSTing to /note, so it works with the script disabled.
// The command channel needs JavaScript because a drag is not a form
// submission; a comment is, and making it depend on the script would
// add a failure mode for no gain.
note_form(
2026-08-06 15:32:09 +02:00
s,
note_to,
"why this move, what is unclear, what is annoying \u{2014} bound to this position",
CB-WP-0027 T01-T04: the commentary track The meta view beside the table, and a note channel that provably cannot carry a move. T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was easy is that PointerFact::parse already refuses any unrecognised field -- a comment could not reach the command path even by accident. So /command carries pointer facts, /note carries text, and Note has no code path to GroundCommand. Comments live in trials/<date>-<slug>.md, not in ScenarioFile: a scenario is executed, replayed and hashed, and prose in it is data the runner must ignore, which is how a format rots. The state hash binds; round and step are for reading. And the retention question, decided before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note reaches ground-game only by being promoted to a register finding, by a human, with the wording chosen then -- "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer. T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a grid child defaults to min-content width and without it the SVG table refuses to shrink and pushes the meta column off-screen, looking correct on the developer's monitor and broken everywhere else. Single-column fallback under 64rem. The running tally moved into the panel so it is visible WHILE PLAYING; it only appeared on the ending page before, and a score you see once the game is over informs nothing. T03. A plain <form method="post">, so the box works with the script disabled; the command channel needs JavaScript because a drag is not a form submission, a comment is one. 303 See Other so a reload does not re-post. esc()'s first hostile input: <script>alert(1)</script> renders escaped AND STILL READABLE -- escaping that eats the player's words is its own defect. Verified over real HTTP: note posted 303, hostile note stored as text, empty note refused 400, game did not advance. T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE I RAN IT: the first version called any note without a recording an orphan, so a live session reported every note as broken -- the recording is only written at game end. A metric that cries wolf is one nobody reads, which is the exact failure this pass exists to prevent. Now ok / pending / orphan, and only orphan is a target-0 number. The self-test exercises the REPORTING path, not just the parser, because design-baseline.py had a green self-test and an unexercised reporting path and that is where it rotted. And a latent Makefile defect surfaced: make trials did nothing, because trials is also a directory and Make saw an up-to-date file. design, difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass -- were ALL missing from .PHONY; only the one that collided revealed it. make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
);
if meta.is_empty() {
return;
}
s.push_str("<h2>this session</h2><div class=\"card\">");
for (i, line) in meta.iter().enumerate() {
if i > 0 {
s.push_str("<br>");
}
s.push_str(&esc(line));
}
s.push_str("</div>");
}
CB-WP-0043: a Problem sits where its Stress lands The picture had become a lie. A row across the middle says "these are the table's" — true under the baseline, false under H2, where three of five cards fall on particular seats. Global goes to the middle, personal beside its owner, and bond on the mean midpoint of the owner's Bond edges. A bond Problem whose owner has no Bond is drawn as personal, because that is the rule — bond_network falls back to the owner at degree 0 — and the picture must agree with the arithmetic. The baseline page is untouched: the scoped layout is taken only when a non-global scope exists, so every prior look at the baseline still holds. The scope reaches the view as a separate marker rather than a field on ProblemView, because the delta says "place owner marker on the card" — a token beside a card — and it is public while the card is face down, which a ProblemView variant could not express. The coverage probe forced a real improvement. It demanded a text token for the new fields and a POSITION is not a token — which is the probe being right: position alone is invisible to text_of and to a screen reader, and illegible when two anchors coincide. So each scoped Problem now says whose it is: everyone's, P1's alone, P2's Bond network, and "P1's alone — no Bond to share it" at degree 0. The fixture gained a third Problem. With two, one of the three placement rules was unexercised and the probe unsatisfiable — a fixture that cannot reach a branch is how a rule ships untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:30:14 +02:00
/// Place each Problem where its Stress lands (CB-WP-0043).
///
/// | scope | where |
/// |---|---|
/// | `global` | the middle of the table — it is everyone's |
/// | `personal` | beside its owner, between that seat and the table |
/// | `bond` | on the owner's Bond lines: the mean midpoint of each Bond edge from the owner |
///
/// **A bond card with no Bonds is drawn as personal**, which is not a
/// rendering convenience — it is the rule. `bond_network` falls back to
/// the owner alone at degree 0, so the picture and the arithmetic agree.
fn scoped_problems(
s: &mut String,
view: &GroundView,
pos: &[(PlayerId, f64, f64)],
cx: f64,
cy: f64,
ry: f64,
) {
use games_ground::edition::StressScope;
let seat_at = |p: PlayerId| {
pos.iter()
.find(|(q, _, _)| *q == p)
.map(|(_, x, y)| (*x, *y))
};
// Bond partners of a seat, for the `bond` anchor. One hop, because
// the LINES are what the card sits on and a line is one edge.
let partners = |owner: PlayerId| -> Vec<(f64, f64)> {
view.relations
.iter()
.filter(|(_, rel)| **rel == games_ground::Relation::Bond)
.filter_map(|(pair, _)| {
let other = if pair.0 == owner {
Some(pair.1)
} else if pair.1 == owner {
Some(pair.0)
} else {
None
};
other.and_then(seat_at)
})
.collect()
};
// Stack cards that land on the same anchor, so two personal Problems
// on one seat do not draw on top of each other.
let mut used: Vec<(f64, f64)> = Vec::new();
for (priority, p) in &view.problems {
let marker = view.problem_markers.get(priority).copied();
let owner = marker.and_then(|m| m.owner);
let (ax, mut ay) = match (marker.and_then(|m| m.scope), owner.and_then(seat_at)) {
// Everyone's: the middle.
(Some(StressScope::Global), _) | (None, _) => (cx, cy - ry * 0.30),
// The owner's: between that seat and the table, so it reads
// as theirs without leaving the felt.
(Some(StressScope::Personal), Some((sx, sy))) => {
(cx + (sx - cx) * 0.52, cy + (sy - cy) * 0.52)
}
// The network's: on the Bond lines out of the owner.
(Some(StressScope::Bond), Some((sx, sy))) => {
let ps = owner.map(partners).unwrap_or_default();
if ps.is_empty() {
// Degree 0 — the rule says personal, so does the picture.
(cx + (sx - cx) * 0.52, cy + (sy - cy) * 0.52)
} else {
let n = ps.len() as f64;
let mx: f64 = ps.iter().map(|(x, _)| (sx + x) / 2.0).sum::<f64>() / n;
let my: f64 = ps.iter().map(|(_, y)| (sy + y) / 2.0).sum::<f64>() / n;
(mx, my)
}
}
// Scoped but ownerless: treat as the table's.
(Some(_), None) => (cx, cy - ry * 0.30),
};
while used
.iter()
.any(|(ux, uy)| (ux - ax).abs() < 40.0 && (uy - ay).abs() < 30.0)
{
ay += 30.0;
}
used.push((ax, ay));
// **Say it, do not only place it.** Position is not legible when
// two anchors coincide and is invisible to `text_of` and to a
// screen reader — so whose Problem this is, is written as well as
// shown. The coverage probe asks for exactly this.
let whose = match (marker.and_then(|m| m.scope), owner) {
(Some(StressScope::Global), _) | (None, _) => "everyone\u{2019}s".to_string(),
(Some(StressScope::Personal), Some(o)) => {
format!("{}\u{2019}s alone", seat_name(o))
}
(Some(StressScope::Bond), Some(o)) => {
if owner.map(partners).unwrap_or_default().is_empty() {
format!(
"{}\u{2019}s alone \u{2014} no Bond to share it",
seat_name(o)
)
} else {
format!("{}\u{2019}s Bond network", seat_name(o))
}
}
(Some(_), None) => "everyone\u{2019}s".to_string(),
};
let scale = 0.46;
let _ = write!(
s,
"<g transform=\"translate({x:.0},{y:.0}) scale({scale})\"><title>{}</title>",
esc(&whose),
x = ax - 120.0 * scale / 2.0,
y = ay - 78.0 * scale / 2.0,
);
problem_svg(s, *priority, p, 0);
let _ = write!(
s,
"<text x=\"60\" y=\"104\" fill=\"#9cf\" font-size=\"22\" \
text-anchor=\"middle\">{}</text>",
esc(&whose)
);
s.push_str("</g>");
}
}
/// The table itself: problems, relationships, seats, solutions, outcome.
///
/// Factored out of [`document`] so [`ending`] shows the SAME table rather
/// than a second rendering of it — two renderings of one state is how
/// they drift.
fn body(s: &mut String, view: &GroundView) {
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
// CB-WP-0028 T03: one overhead view — seats around the edge, the
// Problems and both stacks in the middle, each seat's played card in
// front of it. This replaces three separate diagrams the player had to
// assemble: a Problems row, a relationship circle, and a piles picture.
s.push_str("<h2>the table</h2>");
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
if view.problems.is_empty() {
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
s.push_str("<div class=\"card\">no problems in play</div>");
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
s.push_str(&table_svg(view));
2026-08-08 19:15:52 +02:00
// CB-WP-0046: the placement is legible, the RULE it encodes was not.
//
// Shown only when a non-global scope is actually on the table — under
// the baseline every Problem is everyone's, and an explanation of a
// distinction that is not in play is noise. Same discipline as the
// GROUND modes: at the thing it explains, and closable.
if view.problem_markers.values().any(|m| {
m.scope
.is_some_and(|x| x != games_ground::edition::StressScope::Global)
}) {
if let Some(rule) = stress_scope_rule_text() {
let _ = write!(
s,
"<details class=\"card\"><summary>what a Problem\u{2019}s scope does\
</summary>{}</details>",
esc(&rule)
);
}
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
s.push_str("<h2>seats</h2><div class=\"row\">");
for (id, p) in &view.players {
player_card(s, *id, p, view);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
s.push_str("</div>");
CB-WP-0028 T03/T08: one overhead table, and what the nine observations turned out to be T03. One table_svg: seats around an elliptical table starting at the BOTTOM -- the viewer sits nearest the reader, as at a real table -- Problems and both stacks in the middle, each seat's played card between it and the centre, relations drawn between seats. Two renderers were DELETED: relations_svg and piles_svg. The task said one table not two diagrams, and leaving the old ones would have meant drawing the same thing twice and letting them drift. No coverage probe cost, through a restructure that merged three diagrams and removed two functions. Second confirmation of CB-WP-0027's finding: a probe naming a FACT survives a reflow, one naming a PRESENTATION does not. CB-WP-0024's "17 remaining" broke on a rendering change; this far larger reflow broke nothing. The new control is per seat count -- no two seat circles closer than 70px at 2 through 6 -- asserted rather than eyeballed at three, which is the only count anyone ever looks at. T08 (CB-EV-0026). Seven of nine observations were engine defects, one was a design finding, one was already true and nobody could tell. Observations 4 and 5 both dissolved and had ONE cause: nothing on the page said how drawing works, so a player built a mental model to fill the gap and reported the gap as two feature requests. The import gap was worse than "one of nineteen" -- 5 of 13 columns read from the file we DID vendor, discarded at parse time for eight days. Rule coverage was 59/59 throughout. The gate measures whether rules are EXERCISED; nothing measures whether a player can READ the game, and nothing cheaply could, which is why the person playing it is the instrument. TWO GATES WERE WRITTEN FOR A SMALLER WORLD, and neither was wrong when written. edition-check compared one recorded digest against Problems.csv regardless of which file it described -- correct with one vendored file, comparing across files with four. And a cb-play test asserted the literal "game over" and went red when a won game said "solved", which was T06 working; it now asserts the heading against the OUTCOME and covers the no-outcome case the original never touched. Chaos window 2 closes with zero overrides in eleven declarations at d8. Third and final statement of it: d8 bought rarity by spending evidence, and a mechanism producing no data across a full window cannot be evaluated by that window. make all: exit 0. 57 render tests, 26 cb-play, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:33:57 +02:00
// The discard's contents stay as text: it is public and readable,
// and the stack on the table shows only how many.
let _ = write!(
s,
"<div><span class=\"k\">discard</span> {}</div>",
esc(&cards(&view.solution_discard)),
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
if let Some(o) = &view.outcome {
let personal = o
.personal
.iter()
.map(|(p, v)| format!("{} {v:+}", seat_name(*p)))
.collect::<Vec<_>>()
.join(" ");
let winners = if o.winners.is_empty() {
"nobody".to_string()
} else {
o.winners
.iter()
.map(|w| seat_name(*w))
.collect::<Vec<_>>()
.join(" ")
};
let coalitions = if o.coalitions.is_empty() {
"none".to_string()
} else {
o.coalitions
.iter()
2026-08-07 17:49:06 +02:00
.map(|c| {
format!(
"{} (score {})",
c.members
.iter()
.map(|m| seat_name(*m))
.collect::<Vec<_>>()
.join(" + "),
c.score
)
})
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
.collect::<Vec<_>>()
2026-08-07 17:49:06 +02:00
.join(", ")
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
};
let _ = write!(
s,
"<h2>outcome</h2><div class=\"card\">\
<span class=\"k\">total</span> {total} of {threshold} \
<span class=\"k\">group</span> {group}<br>\
<span class=\"k\">personal</span> {personal}<br>\
<span class=\"k\">coalitions</span> {coalitions}<br>\
<span class=\"k\">mastery</span> {mastery}<br>\
<span class=\"k\">winners</span> {winners}</div>",
total = o.total,
threshold = o.threshold,
group = if o.group_success {
"success"
} else {
"failure"
},
personal = esc(&personal),
coalitions = esc(&coalitions),
mastery = match o.mastery {
Some(m) => format!("{m:+}"),
None => "none".to_string(),
},
winners = esc(&winners),
);
}
}
/// The "your move" section: action cards, numbered fallbacks, the table,
/// and pass. Emits nothing when the seat has nothing legal to do.
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
/// The edition's own words for one Action (F18, ADR-0015 D2).
///
/// `None` if the dataset does not name it, and the page then shows what it
/// always showed — a missing tagline must not blank the card.
fn action_text(a: games_ground::Action) -> Option<games_ground::edition::CardText> {
let want = format!("ACT_{}", format!("{a:?}").to_uppercase());
games_ground::edition::actions()
.ok()?
.into_iter()
.find(|c| c.id == want)
}
2026-08-07 17:49:06 +02:00
/// Who offered this seat their Support (CB-WP-0034).
///
/// **Read off the view, because the command does not carry it.**
/// `RespondToSupport { response }` names the answer and not the person,
/// so no rendering of that command alone can say who — which is why the
/// page could not say it either.
fn supporter_of(view: &GroundView, seat: Option<PlayerId>) -> Option<PlayerId> {
let me = seat?;
view.selections.iter().find_map(|(p, sel)| match sel {
games_ground::view::SelectionView::Shown(s)
if s.action == games_ground::Action::Support && s.target == Some(me) =>
{
Some(*p)
}
_ => None,
})
}
/// The edition's names for the three GROUND modes (GR-A10..A12).
fn ground_mode_label(m: &games_ground::GroundMode) -> &'static str {
use games_ground::GroundMode as M;
match m {
M::Gr => "GROUND \u{2014} Ground & Restate",
M::Ou => "GROUND \u{2014} Observe & Uphold",
M::Nd => "GROUND \u{2014} Name & Decide",
}
}
/// A DARVO target, in words.
fn darvo_target_words(t: &games_ground::DarvoTarget) -> String {
match (t.player, t.problem) {
(Some(p), Some(q)) => format!("{}, Problem {q}", seat_name(p)),
(Some(p), None) => seat_name(p),
(None, Some(q)) => format!("Problem {q}"),
(None, None) => "nothing".to_string(),
}
}
/// What a seat answered a Support with, **and to whom**.
fn support_words(r: &games_ground::SupportResponse, who: Option<PlayerId>) -> String {
use games_ground::SupportResponse as R;
// Branch on whether the seat is KNOWN before phrasing, rather than
// substituting a noun phrase into a possessive: the first draft read
// "accepted a seat this view cannot show's Support".
let Some(who) = who.map(seat_name) else {
let what = match r {
R::AcceptBond => "accepted the Support offered \u{2014} formed a Bond",
R::DeclineBond => "declined the Support offered",
R::FlipToBond => "accepted the Support offered \u{2014} Rivalry became a Bond",
R::BreakRivalry => "broke the Rivalry",
};
return format!("{what} (this view does not show which seat)");
};
match r {
R::AcceptBond => format!("accepted {who}\u{2019}s Support \u{2014} Bond with {who}"),
R::DeclineBond => format!("declined {who}\u{2019}s Support"),
R::FlipToBond => format!("accepted {who}\u{2019}s Support \u{2014} Rivalry became a Bond"),
R::BreakRivalry => format!("broke the Rivalry with {who}"),
}
}
/// The edition's own words for what a DARVO stage does (CB-WP-0045).
///
/// From `DARVO.csv`'s `mandatory_effect`. **Not our paraphrase**: the game
/// says what its stages do and ADR-0015's discipline is to use that.
fn darvo_stage_text(stage: games_ground::DarvoStage) -> Option<String> {
use games_ground::DarvoStage;
let want = match stage {
DarvoStage::Off => return None,
DarvoStage::Deny => "DENY",
DarvoStage::Attack => "ATTACK",
DarvoStage::Reverse => "REVERSE",
};
games_ground::edition::darvo_stages()
.ok()?
.into_iter()
.find(|s| s.stage == want)
.map(|s| s.mandatory_effect)
}
/// The GROUND card's own account of its three modes (CB-WP-0045).
///
/// Shown where the mode is chosen. The card explains GR, OU and ND in one
/// passage; by the Reveal step the action card is no longer on screen, so
/// the explanation has to travel to the decision.
fn ground_modes_text() -> Option<String> {
games_ground::edition::actions()
.ok()?
.into_iter()
.find(|c| c.id.to_uppercase().contains("GROUND"))
.map(|c| c.rules_text)
}
2026-08-08 19:15:52 +02:00
/// What a scope DOES, in the edition's words (CB-WP-0046).
///
/// CB-WP-0045 left this open and gave the wrong reason: *"the scope rule
/// is ours (a Variant), so there is no vendored sentence to render."* The
/// H2 package ships `Rules_Text.csv` and its `Problems / Stress scope`
/// passage is exactly the rule. Nothing read the file.
///
/// **The placement was legible and the rule it encodes was not** — a
/// player could see a card sitting on their Bond lines and have no way to
/// learn that its Stress presses everyone on those lines.
fn stress_scope_rule_text() -> Option<String> {
games_ground::edition::stress_scope_rule()
}
2026-08-07 17:49:06 +02:00
/// A GROUND sub-choice, in words, naming whoever it touches.
fn ground_choice_label(c: &games_ground::GroundChoice) -> String {
use games_ground::GroundChoice as G;
match c {
G::RestoreProblem { problem } => format!("restore Problem {problem}"),
G::CancelAttack { attacker } => {
format!("cancel {}\u{2019}s Attack on you", seat_name(*attacker))
}
G::ProtectProblem { problem } => format!("protect Problem {problem} from Deny"),
G::RemoveBlame { owner } => format!("remove {}\u{2019}s Blame token", seat_name(*owner)),
G::BreakRelation { with } => format!("break your relation with {}", seat_name(*with)),
G::RejectReverse => "reject the Reverse aimed at you".to_string(),
}
}
/// A move button\u2019s words \u2014 and **who it is aimed at** (CB-WP-0034).
///
/// The label was `format!("{c:?}")`. A player deciding whether to accept a
/// Bond read `RespondToSupport { response: AcceptBond }` \u2014 Rust struct
/// syntax with no name anywhere in it \u2014 which is why *"i cant see whos
/// support i accept"* was reported three times across three sessions and
/// survived a whole UI rebuild.
///
/// **Exhaustive on purpose: there is no catch-all arm.** A new command
/// must be given words or this crate stops compiling. A `_ =>` here is
/// how `Debug` output comes back one variant at a time.
///
/// **These are clay-borg\u2019s words, not the edition\u2019s**, and the
/// distinction matters (ADR-0015). The Action cards above carry the
/// game\u2019s own `rules_text`; nothing in the vendored files names these
/// control moves, and `Glossary` is still unvendored (F18). If it lands,
/// this is a caller to revisit.
fn command_label(
c: &games_ground::GroundCommand,
view: &GroundView,
seat: Option<PlayerId>,
) -> String {
use games_ground::GroundCommand as C;
match c {
C::SelectAction {
action,
target,
problem,
} => {
let mut out = format!("{action:?}").to_uppercase();
if let Some(t) = target {
let _ = write!(out, " on {}", seat_name(*t));
}
if let Some(p) = problem {
let _ = write!(out, ", Problem {p}");
}
out
}
C::SpendFreedom => {
"spend your Freedom token \u{2014} act despite the Stress gate".to_string()
}
C::ChooseGroundMode { mode, choice } => {
let named = ground_mode_label(mode);
match choice {
Some(ch) => format!("{named}: {}", ground_choice_label(ch)),
None => named.to_string(),
}
}
// The one the report was about.
C::RespondToSupport { response } => {
use games_ground::SupportResponse as R;
// Not a guess. If the offer is not visible in this view, the
// page says so rather than inventing a name or quietly
// dropping the question -- a confidently wrong seat is worse
// than an honest gap (ADR-0018).
let Some(who) = supporter_of(view, seat).map(seat_name) else {
let what = match response {
R::AcceptBond => "accept the Support offered to you \u{2014} form a Bond",
R::DeclineBond => "decline the Support offered to you",
R::FlipToBond => {
"accept the Support offered to you \u{2014} turn your Rivalry into a Bond"
}
R::BreakRivalry => "break the Rivalry",
};
return format!("{what} (this view does not show which seat)");
};
match response {
R::AcceptBond => {
format!("accept {who}\u{2019}s Support \u{2014} form a Bond with {who}")
}
R::DeclineBond => {
format!("decline {who}\u{2019}s Support \u{2014} the Stress still applies")
}
R::FlipToBond => {
format!("accept {who}\u{2019}s Support \u{2014} turn your Rivalry into a Bond")
}
R::BreakRivalry => {
format!("break your Rivalry with {who}")
}
}
}
C::ChooseDarvoTarget { target } => {
format!("DARVO target: {}", darvo_target_words(target))
}
C::Reveal => "reveal all selections".to_string(),
C::Resolve => "resolve the revealed Actions".to_string(),
C::EndRound => "end the round".to_string(),
}
}
fn move_section(
s: &mut String,
2026-08-07 17:49:06 +02:00
view: &GroundView,
legal: &[games_ground::GroundCommand],
seat: Option<PlayerId>,
may_pass: bool,
) {
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
if !legal.is_empty() {
s.push_str("<h2>your move</h2><div class=\"row\">");
for a in [
games_ground::Action::Investigate,
games_ground::Action::Solve,
games_ground::Action::Support,
games_ground::Action::Attack,
games_ground::Action::Ground,
] {
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
// ADR-0010 D1: the legal targets come from `legal` and are
// written into the page as data. The script matches on them;
// it never derives them. The old text was a CONSTANT —
// "onto a seat, a problem, or the table" — emitted whenever
// any legal command used this action, and it was wrong
// wherever the real target set was narrower, which is almost
// everywhere: Investigate is legal on problems 2 and 3 but
// not 1 (CB-WP-0017).
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
// CB-WP-0018 T03: targets and their meanings, in step, both
// written by Rust. ADR-0010 D1 forbids the page composing the
// second from the first.
let offered: Vec<(String, String)> = seat
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
.map(|seat| {
legal
.iter()
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
.filter_map(|c| {
crate::input::affordance(c, seat)
.filter(|(f, _)| *f == action_id(a))
.map(|(_, t)| (t, crate::input::describe(c, seat)))
})
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
.collect()
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
})
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
.unwrap_or_default();
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
let targets: Vec<String> = offered.iter().map(|(t, _)| t.clone()).collect();
let descs: Vec<String> = offered.iter().map(|(_, d)| d.clone()).collect();
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
if !targets.is_empty() {
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
let _ = write!(
s,
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
"<div class=\"card act pick\" data-drop=\"{id}\" \
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
data-targets=\"{targets}\" data-descs=\"{descs}\">{a:?}<br>\
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
<span class=\"tag\">{tagline}</span>\
<span class=\"k\">onto</span> {names}{rules}</div>",
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
id = action_id(a),
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
targets = esc(&targets.join(" ")),
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
descs = esc(&descs.join("|")),
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
names = esc(&target_names(&targets)),
CB-WP-0028 T01/T02: the cards say what they do ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:22:04 +02:00
// CB-WP-0028 T02 / F18: the card's own words. The
// tagline is always shown; the full rules text is on
// demand, because five of them at once is a wall.
tagline = esc(action_text(a)
.map(|c| c.tagline.clone())
.unwrap_or_default()
.as_str()),
rules = action_text(a)
.map(|c| format!(
"<details><summary>what it does</summary>{}</details>",
esc(&c.rules_text)
))
.unwrap_or_default(),
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
);
}
}
// CB-WP-0045: when a GROUND mode is on offer, the card's own
// account of the three modes travels with the decision. By the
// Reveal step the action card has left the screen, so the player
// was choosing GR / OU / ND from three names alone.
if legal
.iter()
.any(|c| matches!(c, games_ground::GroundCommand::ChooseGroundMode { .. }))
{
if let Some(text) = ground_modes_text() {
let _ = write!(
s,
"<details class=\"card\" open><summary>what the GROUND modes do\
</summary>{}</details>",
esc(&text)
);
}
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
s.push_str("</div><div class=\"row\">");
for (i, c) in legal.iter().enumerate() {
let spatial = seat.is_some_and(|seat| crate::input::affordance(c, seat).is_some());
if !spatial {
let _ = write!(
s,
fix: a click target wearing a drag affordance made the controls look dead Tier S (a fix inside a boundary; chaos d8=7 from the previous roll stands for this continuation). Two observations from play that are ONE defect. `play again`, `end session`, `pass` and the move buttons carried `.pick`, which is cursor:grab. The stylesheet has .btn{cursor:pointer} BEFORE .pick{cursor:grab}, so grab won. A GRAB CURSOR INVITES A DRAG. A drag released over nothing posts nothing, so the player picked up the button, let go, and the page did nothing. It looked dead because the affordance told them to do the one thing that does not work. Reported as two separate things -- "the button shows a hand to pick up that it probably shouldn't" and "I can't start another game or stop the server" -- and the first causes the second. The click path itself was never broken: driving again->again and done->done through the JS harness posts correctly. The logic was fine and the invitation was wrong. Click targets now carry `.tap` -- pointer cursor, same press affordance. This extends CB-WP-0017's rule (interactive and inert must not look identical) to: click and drag must not look identical either. The test asserts both directions, because checking only that buttons lost `.pick` would pass for a page with no affordances at all. Registered F20 (applied) and F21. F21 IS THE ONE I COULD NOT REPRODUCE: dragging did not work until after the first note was saved. Ruled out the plausible mechanisms -- the gesture logic posts correctly against the served page, the drag ghost carries pointer-events:none so it cannot intercept the drop, and the markup is identical before and after since the 303 re-renders the same page from the same state. Remaining candidates are a <details> toggle shifting layout mid-drag, a first-load timing difference, or browser-level pointer capture. Reproducing it needs a browser, which no test here has -- the same gap F19 named. Recorded as unreproduced rather than given a speculative fix. And the fourth observation is confirmation, not a bug: "drawing my cards from the deck is not implemented, I did not need to do that" is exactly what CB-WP-0028 T04 determined and deliberately did not build. It is the first evidence that importing the card text closed the comprehension gap that produced the earlier click-the-deck request. make all: exit 0. 62 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:25:06 +02:00
"<div class=\"card btn tap\" data-drop=\"cmd-{i}\">{}</div>",
2026-08-07 17:49:06 +02:00
esc(&command_label(c, view, seat))
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
);
}
}
fix: the game was unplayable, and the note redirect was refused Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:51:35 +02:00
// CB-WP-0028: the table's drop zone is THE TABLE, drawn above.
// This was a separate card labelled "the table" sitting among the
// move buttons, which is why a player looking at a picture of a
// table could not find anywhere to drop.
s.push_str("</div>");
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
if may_pass {
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
s.push_str(
fix: a click target wearing a drag affordance made the controls look dead Tier S (a fix inside a boundary; chaos d8=7 from the previous roll stands for this continuation). Two observations from play that are ONE defect. `play again`, `end session`, `pass` and the move buttons carried `.pick`, which is cursor:grab. The stylesheet has .btn{cursor:pointer} BEFORE .pick{cursor:grab}, so grab won. A GRAB CURSOR INVITES A DRAG. A drag released over nothing posts nothing, so the player picked up the button, let go, and the page did nothing. It looked dead because the affordance told them to do the one thing that does not work. Reported as two separate things -- "the button shows a hand to pick up that it probably shouldn't" and "I can't start another game or stop the server" -- and the first causes the second. The click path itself was never broken: driving again->again and done->done through the JS harness posts correctly. The logic was fine and the invitation was wrong. Click targets now carry `.tap` -- pointer cursor, same press affordance. This extends CB-WP-0017's rule (interactive and inert must not look identical) to: click and drag must not look identical either. The test asserts both directions, because checking only that buttons lost `.pick` would pass for a page with no affordances at all. Registered F20 (applied) and F21. F21 IS THE ONE I COULD NOT REPRODUCE: dragging did not work until after the first note was saved. Ruled out the plausible mechanisms -- the gesture logic posts correctly against the served page, the drag ghost carries pointer-events:none so it cannot intercept the drop, and the markup is identical before and after since the 303 re-renders the same page from the same state. Remaining candidates are a <details> toggle shifting layout mid-drag, a first-load timing difference, or browser-level pointer capture. Reproducing it needs a browser, which no test here has -- the same gap F19 named. Recorded as unreproduced rather than given a speculative fix. And the fourth observation is confirmation, not a bug: "drawing my cards from the deck is not implemented, I did not need to do that" is exactly what CB-WP-0028 T04 determined and deliberately did not build. It is the first evidence that importing the card text closed the comprehension gap that produced the earlier click-the-deck request. make all: exit 0. 62 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:25:06 +02:00
"<div class=\"card btn tap\" data-drop=\"pass\">pass \u{2014} decline to act</div>",
CB-WP-0017: legible interaction, and the chaos window's verdict Provenance (tier M, structural S, chaos d4=4 -> OVERRIDE drawn M): the maintainer could drag after CB-WP-0016 but could not tell what was pickable, held, or droppable. Underneath that, the page was WRONG about which moves exist: 9 legal commands rendered as 5 cards each claiming all three target kinds, from a const string in the emitter. Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2 or 3. The live page now says 'Solve onto problem 1'. ADR-0010 restates control 5, which this work would otherwise have outgrown in silence: every game fact the page acts on must arrive from Rust as data; the script may read, match and render it, never compute, infer, filter or default one. The survey's real finding is that the permitted and forbidden designs are indistinguishable from outside, so the vocabulary grep is demoted to a cheap first line and two behavioural properties become the controls -- the highlighted set EQUALS the set Rust emitted, and anything the page marks legal must resolve. Both mutation-proven; the derive-legality mutation produces a plausible highlight (seat-0,1,2 where only seat-1 is legal) and is caught. Visible now: .pick resting shadow, .held on the grabbed element, .dropok on every legal target including BOTH drawings of a seat, and a ghost following the pointer. Nothing perceptual is verified and ADR-0010 D5 says so. The DOM stub now models classList/querySelectorAll/createElement and builds its node set from the real emitted page. Trap recorded: QuickJS fixes its stack limit at Context creation relative to that frame, so a helper returning a Context makes every later eval report 'SyntaxError: stack overflow'. CHAOS WINDOW CLOSED, 12 declarations, 2 overrides, one each way. Both changed the outcome, so the retirement condition is not met. Verdict: keep, and recommend d4 -> d8 with a second window of 12 -- that is a change to the loop's own constraints and is owed to the next declaration as tier-M work, not made here. CB-EV-0014 corrected: it quoted CB-WP-0015 at $15.14/136 and called it the first settled figure quoted. Now $22.70/166. The number had been read during CB-WP-0015 itself, so there are two defects -- the boundary, and quoting from memory instead of re-running the instrument. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:42:45 +02:00
);
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
}
}
/// Quote a string as a JSON literal, so a token can never end the script.
fn json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'<' => out.push_str("\\u003c"),
'>' => out.push_str("\\u003e"),
'&' => out.push_str("\\u0026"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
/// Extract the document's visible text and element ids.
///
/// ADR-0007 control 6 requires the coverage gate to assert over the
/// **parsed emitted document**, not over the Rust that emits it. This is
/// that parse: it drops markup and returns what a reader would see, plus
/// the ids a pointer can address. A substring search over the raw source
/// would happily find a token inside a comment or a style rule.
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
/// Every `data-drop="…"` value in the document.
///
/// CB-WP-0016. A real parse of the attribute rather than a substring
/// search: `html.contains("seat-1")` would be satisfied by the *text*
/// "seat-1" and by `data-drop="seat-10"`, and the point of the check this
/// feeds is that an affordance can name a target that is not there.
///
/// **Drop keys are `data-drop`, not `id`, and that is the fix for
/// CB-WP-0016.** An `id` must be unique in a document, so exactly one
/// element could ever be `seat-0` — the relationship-graph circle took it
/// and the seat card the instruction text points at went without. A seat
/// is drawn twice and both drawings are the seat.
/// One line of the game log: what was done, and what it produced.
///
/// Built by the caller from `bot::Applied` so this crate stays free of
/// the driver, and phrased in the **recorder's** vocabulary via
/// `record::to_step` — CB-WP-0018 T02 forbids a fourth phrasing, because
/// what the player reads should be what the scenario file will say.
pub struct LogLine {
pub who: String,
pub what: String,
/// Rendered effects. **Empty is the case that matters:** a command
/// that produced no events is the one the player cannot otherwise
/// account for.
pub effects: Vec<String>,
}
/// One thing the player said, placed in the account (CB-WP-0032).
///
/// **`after` is a count of commands, not a round.** A note anchored to
/// `round`/`step` cannot be ordered against the log, because several
/// commands share a step and the log is a sequence of commands — placing
/// by round would put a note *somewhere in* the right round, which for a
/// remark about a specific move is the wrong position stated confidently.
/// The server knows exactly how many commands had been played when the
/// note was written, so that is what it records.
#[derive(Debug, Clone)]
pub struct LogNote {
/// Commands played when this was written; `0` is before any move.
pub after: usize,
pub round: u8,
/// Where in the round, or `after the end` for a post-game note.
pub step: String,
pub text: String,
}
/// The account of the session: what happened, and what the player said
/// about it, in one sequence.
///
/// **A struct rather than two parameters** because both pages take both
/// and `document_with_log` was already at seven arguments — and because
/// the two are only meaningful together, which is the point of showing
/// them interleaved.
#[derive(Clone, Copy)]
pub struct Account<'a> {
pub lines: &'a [LogLine],
pub notes: &'a [LogNote],
}
impl<'a> Account<'a> {
/// An account with no commentary — for callers that have no notes.
pub fn of(lines: &'a [LogLine]) -> Self {
Self { lines, notes: &[] }
}
}
/// The game log, newest last, with the empty case spelled out.
///
/// **The player's own comments are interleaved** (CB-WP-0032) at the
/// point they were written, because a remark like *"why did that do
/// nothing?"* is about the move above it and loses its subject anywhere
/// else.
///
/// **They are marked as the player's, and cannot be read as game
/// events.** A note carries no `who`/`what` and renders in its own
/// `.note` block with the player's words in quotes: the log is the
/// recorder's vocabulary — what the scenario file will say — and a
/// comment that looked like a log line would be a sentence the game never
/// produced, sitting in the account of what the game did.
fn log_section(s: &mut String, log: Account<'_>) {
fn note_block(s: &mut String, n: &LogNote) {
let _ = write!(
s,
"<div class=\"note\"><span class=\"k\">you \u{2014} round {round}, \
{step}</span><br>\u{201c}{text}\u{201d}</div>",
round = n.round,
step = esc(&n.step),
text = esc(&n.text),
);
}
let notes_after = |k: usize, s: &mut String| {
for n in log.notes.iter().filter(|n| n.after == k) {
note_block(s, n);
}
};
s.push_str("<h2>log</h2><div class=\"card\" id=\"cb-log\">");
if log.lines.is_empty() && log.notes.is_empty() {
s.push_str("<span class=\"k\">nothing has happened yet</span>");
}
// Anything said before the first move still belongs in the account.
notes_after(0, s);
for (i, line) in log.lines.iter().enumerate() {
let _ = write!(
s,
"<div><span class=\"k\">{who}</span> {what}",
who = esc(&line.who),
what = esc(&line.what),
);
if line.effects.is_empty() {
// The silence CB-WP-0018 was reported for, said out loud.
s.push_str(" \u{2014} <span class=\"nil\">no effect</span>");
} else {
for e in &line.effects {
let _ = write!(s, "<br><span class=\"eff\">\u{2192} {}</span>", esc(e));
}
}
s.push_str("</div>");
notes_after(i + 1, s);
}
// A note can outrun the log: post-game notes are written when every
// command has been played, and one written against a longer log than
// this page has must still appear rather than be silently dropped.
for n in log.notes.iter().filter(|n| n.after > log.lines.len()) {
note_block(s, n);
}
s.push_str("</div>");
}
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
/// Who did what, at the end (CB-WP-0028 T07).
///
/// **Two kinds of line, and they are labelled differently.** A *fact* is
/// something the game counts — Problems claimed, personal score, Stress,
/// Blame. A *reading* is ours, and says so.
///
/// **In SHARED GROUND the game defines no ranking at all**: `Modes.csv`
/// gives `scoring_tiebreak` as *"Not applicable"*, because the table
/// succeeds or fails together. So contributions are shown and **not
/// ordered**, and the page says why. Ranking a co-operative game because
/// a leaderboard is easy to draw would be inventing scoring the game does
/// not have — the same defect as canonising a provisional default.
fn rankings(s: &mut String, view: &GroundView) {
let Some(outcome) = &view.outcome else {
return;
};
let mode_id = match view.mode {
games_ground::ScoringMode::SharedGround => "MODE_COOP",
games_ground::ScoringMode::CommonProblem => "MODE_SEMI",
games_ground::ScoringMode::BondedCoalitions => "MODE_COALITION",
};
let tiebreak = games_ground::edition::modes()
.ok()
.and_then(|m| m.into_iter().find(|(c, _)| c.id == mode_id))
.map(|(_, tb)| tb)
.unwrap_or_default();
let ranked = !tiebreak.trim().is_empty() && !tiebreak.starts_with("Not applicable");
// A fact the game counts: who claimed which Problem.
let solved = |p: PlayerId| {
view.problems
.values()
.filter(|q| matches!(q, ProblemView::FaceUp { claimed_by: Some(c), .. } if *c == p))
.count()
};
let mut seats: Vec<PlayerId> = view.players.keys().copied().collect();
if ranked {
// The GAME's tiebreak, not ours: lower Stress first, which both
// ranked modes name as their first key.
seats.sort_by_key(|p| (view.players[p].stress, std::cmp::Reverse(solved(*p))));
}
let _ = write!(
s,
"<h2>who did what</h2><div class=\"card\"><span class=\"k\">mode</span> {mode}",
mode = esc(&format!("{:?}", view.mode)),
);
if ranked {
let _ = write!(
s,
"<br><span class=\"k\">ranked by the game&#39;s own tiebreak</span> {tb}",
tb = esc(&tiebreak),
);
} else {
s.push_str(
"<br><span class=\"k\">not ranked</span> \
this mode&#39;s scoring_tiebreak is \u{201c}Not applicable\u{201d} \u{2014} \
the table succeeds or fails together, so these are contributions, \
not a leaderboard",
);
}
s.push_str("</div><div class=\"row\">");
for p in &seats {
let pv = &view.players[p];
let _ = write!(
s,
"<div class=\"card\"><b>{name}</b><br>\
<span class=\"k\">problems solved</span> {n}<br>\
<span class=\"k\">personal</span> {score}<br>\
<span class=\"k\">stress</span> {stress} \
<span class=\"k\">blame</span> {blame}</div>",
name = seat_name(*p),
n = solved(*p),
score = outcome
.personal
.get(p)
.map(|v| format!("{v:+}"))
.unwrap_or_else(|| "\u{2014}".into()),
stress = pv.stress,
blame = pv.blame_from.len(),
);
}
s.push_str("</div>");
// A READING, marked as ours. Only where the game ranks at all, and
// only when there is a clear leader -- a tie is shown as a tie.
if ranked {
let best = seats.iter().map(|p| solved(*p)).max().unwrap_or(0);
let leaders: Vec<String> = seats
.iter()
.filter(|p| solved(**p) == best && best > 0)
.map(|p| seat_name(*p))
.collect();
let _ = write!(
s,
"<div class=\"card\"><span class=\"k\">clay-borg&#39;s reading, not a rule</span> {who}</div>",
who = match leaders.len() {
0 => "nobody claimed a Problem".to_string(),
1 => format!("most Problems solved: {}", leaders[0]),
_ => format!("most Problems solved, tied: {}", leaders.join(", ")),
},
);
}
}
/// The page a finished game leaves behind (CB-WP-0018 T01).
///
/// `view` is `None` when the game ended badly: then there is no result to
/// draw and saying so is the whole point. Drawing a table for a game that
/// crashed would be the same lie the empty page told, dressed up.
///
/// **This page does not auto-reload.** The old one reloaded on `ok` and
/// the reload was refused, which is how a completed game became a blank
/// tab.
///
/// **`note_to` is `Some` when a note can still be anchored** (CB-WP-0031).
/// The comment box used to stop existing the moment the game ended, which
/// removed the channel exactly when a player has most to say — they have
/// just seen the outcome, and *"so that's why that failed"* is the reading
/// the whole trial protocol wants. It is `None` only when there is no
/// final position to bind a note to, and then the page says so rather than
/// showing a box that would refuse.
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
pub fn ending(
view: Option<&GroundView>,
message: &str,
endpoint: &str,
note_to: Option<&str>,
alive: &str,
log: Account<'_>,
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
series: &[String],
) -> String {
let mut s = String::with_capacity(4096);
let _ = write!(
s,
"<!doctype html><meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
<title>GROUND {heading}</title><style>{STYLE}</style>\
<h1>{heading}</h1><div class=\"card\">{msg}</div>",
msg = esc(message),
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
// CB-WP-0028 T06. "Game over" is arcade vocabulary for a failure
// state. GROUND is co-operative and about repairing a situation:
// when the table clears its threshold it SOLVED something, and
// telling a player "game over" for that describes what they did
// wrongly. A game with no outcome claims neither.
heading = match view.and_then(|v| v.outcome.as_ref()) {
Some(o) if o.group_success => "game solved",
Some(_) => "game over",
None => "the game stopped",
},
);
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
// CB-WP-0028 T05: the ending page gets the table's shape — the
// result on the left, everything about the session on the right.
s.push_str("<div class=\"cb-cols\"><div class=\"cb-game\">");
match view {
Some(v) => body(&mut s, v),
None => {
s.push_str(
"<div class=\"card\">The game ended without a result, so there \
is no final table to show. The reason is above.</div>",
);
}
}
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
s.push_str("</div><div class=\"cb-meta\">");
if let Some(v) = view {
rankings(&mut s, v);
}
CB-WP-0024: the table you can watch Four of the maintainer's five playtest remarks. Three of the five turned out to be data the projection already carried, rendered as text -- the table's problem was legibility, not content, and the coverage gate passes either way because it proves nothing is OMITTED, not that anything is readable. That gap is named in the evidence rather than closed: the honest control is a person playing it. T01. The ending control was two defects wearing one button. The label said "close -- I have read this" while hotseat.rs reads `done` as STOP THE SERVER, and acknowledging it changed nothing -- the tab kept a full table and a `play again` pointing at a closed port. Now labelled by its effect, and the page seals itself on the `closed` reply: removeAttribute on every control's data-drop, so they stop being droppable by the same rule that made them droppable. removeAttribute rather than setAttribute(_, null), which writes the truthy string "null" in a browser. The reason it survived is structural. jsrun's fetch stub returned {then: function(){return this}} and never invoked its callbacks, so every line of the script reacting to the server was unreachable from every test in this project -- a page that ignores the server was indistinguishable from one that acts on it. Same finding as CB-WP-0016's "a stub too thin to express a failure is how the failure survives", one layer deeper, at the reply. The stub now delivers a real then-chain; gesture_with_reply reports surviving controls; the seal is mutation-proven and a negative control asserts `ok: dealing` does NOT seal. T02. Draw and discard as offset stacks with counts. The shuffle question the task required settling: it already works, at games/ground/src/lib.rs:1419-1435, implementing the U4 default that ground-game confirmed 2026-08-03. Nothing raised. The piles show the state before it fires, which is derivable from the view; a claim that a reshuffle HAS happened is not, and is not made. CB-WP-0026 applied that ruling the same day this consumed it -- first time answering "is this underdetermined?" was one lookup instead of a message. T03. Each seat's play drawn as a card, sentence kept beside it. The face-down back is a const with no parameters: SelectionView::Hidden carries nothing, so there is no data path into the back to add later. The leak test copies view.rs's own shape -- identical backs across two different hidden situations, THEN assert a revealed play does show, because without the second half the first passes for a renderer that draws nothing. T04. MatchTally lives in `play`, beside the listener and the seed. What "cumulative" means was decided before anything was summed, and the answer is that GROUND defines one game and no series: summed personal score and games-won answer different questions, and a test asserts they can point at different seats. Both shown, both labelled. Registered F15 as a NOTE -- the test shows the tallies can differ, which is arithmetic, not evidence the ambiguity harms play, so GameDesign §3.1 bars reporting it. First use of the note tier since D6 wrote it, and it came from building rather than from play. make all: exit 0. 41 render tests, 26 cb-play tests, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:32:48 +02:00
// CB-WP-0024 T04. Above the log, because it is a result and the log is
// the account. Empty for a first game — one game is not a series, and
// a "cumulative" panel restating the outcome above it is noise.
if !series.is_empty() {
s.push_str("<h2>this session</h2><div class=\"card\">");
for (i, line) in series.iter().enumerate() {
if i > 0 {
s.push_str("<br>");
}
s.push_str(&esc(line));
}
s.push_str("</div>");
}
// CB-WP-0031: above the log and below the result, because that is the
// order the thought arrives in — you see how it ended, you say what
// you make of it, and the account is there to check yourself against.
match note_to {
Some(to) => note_form(
&mut s,
to,
"how did that go \u{2014} what surprised you, what was unclear, \
what would you do differently",
),
None => s.push_str(
"<div class=\"card\">No comment box: the game stopped without a \
final position, so there is nothing to bind a note to.</div>",
),
}
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
// Observations 6 and 7: the controls and the full log belong beside
// 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,
CB-WP-0020: the table you can read Six of seven perceptual defects fixed; item 1 already passed. T01, at the maintainer's instruction: a legal target restyles its EXISTING border rather than drawing a new box. outline + outline-offset drew a second rectangle, which an SVG viewport clips (the missing top and left edges) and which made a seat's highlight card-sized. A border already in the layout cannot move the layout. T02: the ghost was a textContent copy of the card, which is why the line break collapsed and it read as a second card, and why showing the explanation destroyed the label. It is now a pill, the explanation is appended beside the label, and the left-behind element is dimmed and dashed. The stub grew innerHTML so a test can assert BOTH are present -- it could previously only see that something was displayed. T03: NOT reproduced and recorded as not reproduced. The likeliest cause is which element the browser reports -- for touch and pen the pointer is captured to the pointerdown target, making every drop look like a drop-on-itself, which is the other half of the report. elementFromPoint is correct under both explanations. Separately the refusal was written in element ids on the one surface a player reads when something goes wrong; it now speaks the game's words and a test forbids id leakage. T04: seat selections rendered as Debug. The coverage gate then failed my first fix for dropping a field when target and problem were both set -- the aggregate does not produce that shape and the gate was right not to care. T05: the headline reads from group_success. 'Play again' is real, and its first version was useless: run_game bound a fresh listener per game, so a second game moved to a new port and left the tab pointing at a dead one. One listener per session now, and the test asserts the second game is a DIFFERENT deal. Chaos d8=8 fired the first override at the new rate and drew S, changing nothing -- one half of window 2's retirement condition. CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight, and the first under 20%. make all exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:20:38 +02:00
"<div class=\"row\">\
fix: a click target wearing a drag affordance made the controls look dead Tier S (a fix inside a boundary; chaos d8=7 from the previous roll stands for this continuation). Two observations from play that are ONE defect. `play again`, `end session`, `pass` and the move buttons carried `.pick`, which is cursor:grab. The stylesheet has .btn{cursor:pointer} BEFORE .pick{cursor:grab}, so grab won. A GRAB CURSOR INVITES A DRAG. A drag released over nothing posts nothing, so the player picked up the button, let go, and the page did nothing. It looked dead because the affordance told them to do the one thing that does not work. Reported as two separate things -- "the button shows a hand to pick up that it probably shouldn't" and "I can't start another game or stop the server" -- and the first causes the second. The click path itself was never broken: driving again->again and done->done through the JS harness posts correctly. The logic was fine and the invitation was wrong. Click targets now carry `.tap` -- pointer cursor, same press affordance. This extends CB-WP-0017's rule (interactive and inert must not look identical) to: click and drag must not look identical either. The test asserts both directions, because checking only that buttons lost `.pick` would pass for a page with no affordances at all. Registered F20 (applied) and F21. F21 IS THE ONE I COULD NOT REPRODUCE: dragging did not work until after the first note was saved. Ruled out the plausible mechanisms -- the gesture logic posts correctly against the served page, the drag ghost carries pointer-events:none so it cannot intercept the drop, and the markup is identical before and after since the 303 re-renders the same page from the same state. Remaining candidates are a <details> toggle shifting layout mid-drag, a first-load timing difference, or browser-level pointer capture. Reproducing it needs a browser, which no test here has -- the same gap F19 named. Recorded as unreproduced rather than given a speculative fix. And the fourth observation is confirmation, not a bug: "drawing my cards from the deck is not implemented, I did not need to do that" is exactly what CB-WP-0028 T04 determined and deliberately did not build. It is the first evidence that importing the card text closed the comprehension gap that produced the earlier click-the-deck request. make all: exit 0. 62 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:25:06 +02:00
<div class=\"card btn tap\" data-drop=\"again\">play again</div>\
<div class=\"card btn tap\" data-drop=\"done\">end session \u{2014} stops the game server</div>\
CB-WP-0028 T04-T07: a game you solve, rankings that cite their source, and a feature deliberately not built T04 BUILT NOTHING, WHICH IS THE CORRECT OUTCOME. "Click the draw stack to get hand cards" is not a legal move: GroundCommand has no standalone draw, and the edition's own INVESTIGATE text settles it -- "Choose one hidden, non-Denied Problem and reveal it. THEN DRAW ONE SOLUTION." Drawing is a consequence, never an action. Implementing click-to-draw would have invented a rule, which is what CB-WP-0023 exists to stop. And observation 5 is already true: drawing is ALREADY automatic, inside INVESTIGATE, with no player input. An auto-draw option was asked for a thing that has never been manual. Both observations have one root cause and T02 fixed it. The maintainer expected to take cards from the deck because NOTHING ON THE PAGE SAID HOW DRAWING WORKS -- the INVESTIGATE card's own text was in a file we had not imported. No finding raised: a player's instinct differing from a legible rule is a comprehension gap, not a rules gap. Whether the instinct recurs now that the text is present is a testable question and was not before. T06: "game solved" on a win, "game over" on a loss, "the game stopped" with no outcome. Asserted all three ways, because a test checking only the win case passes for a page that always says solved. More than tone -- GROUND is co-operative and about repairing something, and arcade failure-state vocabulary for a win tells a player the wrong thing about what they just did. T05: the ending page gets the table's two-column shape. Result left; rankings, controls and the full log right. The seal still removes every control wherever they now live. T07's interesting decision was NOT TO RANK. Modes.csv defines scoring_tiebreak per mode, so ordering is the GAME's where one exists -- "Lower combined Stress, then fewer Blame tokens" for coalitions. For SHARED GROUND it says "Not applicable", because the table succeeds or fails together. So co-op shows contributions and refuses to order them, and says why. Drawing a leaderboard because a leaderboard is easy would invent scoring the rules do not have. The one derived superlative is labelled "clay-borg's reading, not a rule" and appears only where the mode ranks; ties are shown as ties. 52 render tests pass. check clean, loop-lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:25:48 +02:00
</div>",
);
s.push_str("</div></div>");
let _ = write!(
s,
"<div id=\"cb-status\">the game is over</div>\
<script>window.CB_ENDPOINT={endpoint};window.CB_ALIVE={alive}</script>\
<script>{SCRIPT}</script>",
endpoint = json_string(endpoint),
alive = json_string(alive),
);
s
}
CB-WP-0016: the drop target that was never there Provenance (tier S, one paragraph in lieu of survey and ADR): the human check that kept INTENT stage 1 open was run and the drag was broken. Root cause, worth more than the instance: drop targets were ids, and an id must be unique, so exactly one element could ever be seat-0. The relationship-graph circle took it and the seat card that every action card's own text points at -- 'drag Attack onto a seat' -- silently had none. A seat is drawn twice and both drawings are the seat; the document model could not express that. Drop keys are now data-drop. Any number of elements may carry the same key, so a seat is droppable on its card and on its graph node. Measured on a live server: seat-0/1/2 each appear twice, id survives only on cb-status which is the one element the script looks up, and down=action-attack&up=seat-1 returns ok. Second defect: a drop on nothing returned without posting and without touching the status line, so a broken target was indistinguishable from a working page. resolve already refuses rather than defaulting, which is right; refusing SILENTLY is not. The page now reports the raw fact -- 'took action-attack, let go over nothing droppable' -- which names elements, not moves, so ADR-0007 control 5 holds. And the honest part: the general check added here -- every offered affordance names a key that exists, driven through Policy::choose over four real bot games -- does NOT catch the reported defect. seat-0 did exist, on the graph circle. It is kept because a wholly absent target is a real class, and paired with a targeted regression test that does catch it. Three mutations, each red for its stated reason, including the reported defect reintroduced; only the targeted test fires on that one. A cb-play assertion matched id="action-ground" as a substring while describing itself as checking the page; rewritten through drop_keys. make all exits 0. Stage 1 stays open: verified by tests, mutation and a live server, not by a human dragging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00
pub fn drop_keys(html: &str) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
let mut rest = html;
while let Some(i) = rest.find("data-drop=\"") {
let after = &rest[i + 11..];
match after.find('"') {
Some(j) => {
out.insert(after[..j].to_string());
rest = &after[j..];
}
None => break,
}
}
out
}
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat play, at a measured marginal AM-4a cost of zero. games-ground shipped: 23 third-party crates cb-render-html: 23 third-party crates new crates introduced: 0 Measured, not asserted — the survey's own lesson. AM-4a is unmoved at 246,250; own source is 7,636 -> 9,652. What shipped: crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship graph), input.rs (pointer facts -> commands), serve.rs (Guard, Request, loopback bind) tools/cb-play hotseat.rs + `--serve PORT` Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null. The renderer targets the existing Project trait; the port waits for stage 2's wgpu implementation to be its second use. The six controls, all live, all mutation-checked (8 mutations, each red for its stated reason): 1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind 4 a token-less request is refused, in the unit AND over a real socket 5 JS may not construct commands — the page reports pointer facts, Rust resolves them against the legal list the aggregate already offered, and a test asserts the emitted script contains no game vocabulary 6 the coverage gate crosses the language boundary: it walks the serialized view for leaf paths and requires each token to appear in the PARSED emitted document, with a test that the parse really is a parse (script/style contents must not count as rendered) The gate fired on its author again, on its first run: ground_choices.*. choice, ground_choices.*.problem and players.*.blame_from were in neither list. The last is the one worth keeping — an EMPTY vector is a leaf path of its own, and it now renders as an explicit absence. Also, a mutation that did not go red: removing the Sec-Fetch-Site arm alone left the cross-site test green, because the Origin check caught it independently. Both had to be removed before the control bit. Recorded because a control that passes for a reason you did not intend has not been demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
pub fn text_of(html: &str) -> String {
let mut out = String::with_capacity(html.len() / 2);
let bytes: Vec<char> = html.chars().collect();
let mut i = 0;
let mut skip_to: Option<&str> = None;
while i < bytes.len() {
if bytes[i] == '<' {
// Find the tag name.
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != '>' {
j += 1;
}
let tag: String = bytes[start..j.min(bytes.len())].iter().collect();
let lower = tag.to_ascii_lowercase();
let name = lower
.trim_start_matches('/')
.split([' ', '\t', '\n', '>'])
.next()
.unwrap_or("")
.to_string();
if let Some(want) = skip_to {
if lower.starts_with('/') && name == want {
skip_to = None;
}
} else if name == "script" || name == "style" {
// Their contents are not text a reader sees.
if !lower.starts_with('/') && !lower.ends_with('/') {
skip_to = Some(if name == "script" { "script" } else { "style" });
}
} else {
// Ids are addressable surface, so they count as rendered.
if let Some(k) = lower.find("id=\"") {
let rest = &tag[k + 4..];
if let Some(end) = rest.find('"') {
out.push(' ');
out.push_str(&rest[..end]);
}
}
}
i = j + 1;
continue;
}
if skip_to.is_none() {
out.push(bytes[i]);
}
i += 1;
}
// Unescape the entities esc() introduced, so a test looks for the text
// a reader sees rather than its encoding.
out.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&#39;", "'")
.replace("&amp;", "&")
}