From a733826d9501584f76a14329b0671affd2c19959 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 02:24:13 +0200 Subject: [PATCH] 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 --- crates/cb-render-html/src/doc.rs | 43 ++++- crates/cb-render-html/src/input.rs | 34 ++++ crates/cb-render-html/src/jsrun.rs | 98 +++++++++- .../CB-EV-0016-the-browser-is-a-client.md | 182 ++++++++++++++++++ gates.toml | 6 +- specs/InnerLoop.md | 21 +- specs/InnerLoopReference.md | 32 +++ tools/cb-play/src/hotseat.rs | 2 +- .../CB-WP-0018-the-browser-is-a-client.md | 36 +++- 9 files changed, 424 insertions(+), 30 deletions(-) create mode 100644 evidence/CB-EV-0016-the-browser-is-a-client.md diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index 351176f..35c43a8 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -56,7 +56,7 @@ fn cards(list: &[games_ground::SolutionCard]) -> String { /// The only JavaScript in the project. See the module docs. pub const SCRIPT: &str = r#" (function () { - var down = null, held = null, marked = [], ghost = null; + var down = null, held = null, marked = [], ghost = null, ghostLabel = ''; function node(e) { var n = e.target; @@ -70,6 +70,19 @@ pub const SCRIPT: &str = r#" // 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. + // 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; + } + function mark(n) { var spec = n.getAttribute('data-targets'); if (!spec) { return; } @@ -88,6 +101,7 @@ pub const SCRIPT: &str = r#" if (held) { held.classList.remove('held'); held = null; } if (ghost && ghost.parentNode) { ghost.parentNode.removeChild(ghost); } ghost = null; + ghostLabel = ''; down = null; } @@ -102,7 +116,8 @@ pub const SCRIPT: &str = r#" if (n.getAttribute('data-targets')) { ghost = document.createElement('div'); ghost.id = 'cb-ghost'; - ghost.textContent = n.textContent; + ghostLabel = n.textContent; + ghost.textContent = ghostLabel; ghost.style.left = e.clientX + 'px'; ghost.style.top = e.clientY + 'px'; document.body.appendChild(ghost); @@ -113,6 +128,11 @@ pub const SCRIPT: &str = r#" if (!ghost) { return; } ghost.style.left = e.clientX + 'px'; ghost.style.top = e.clientY + 'px'; + // The explanation, beside the pointer and therefore beside the + // target it is over (CB-WP-0018 T03). + var over = describeOf(held, key(node(e))); + ghost.textContent = over || ghostLabel; + ghost.className = over ? 'over' : ''; }); document.addEventListener('pointercancel', clear); @@ -165,6 +185,7 @@ h1,h2{font-size:1rem;margin:1.2rem 0 .4rem;color:#9cf} .dropok{outline:2px dashed #9cf;outline-offset:3px;background:#1d2a33} .dropok text{fill:#cfe} /* The ghost that follows the pointer, so a drag is not invisible. */ +#cb-ghost.over{background:#1d3347;border-color:#9cf;color:#cfe} #cb-ghost{position:fixed;pointer-events:none;z-index:9;padding:.3rem .5rem; border-radius:6px;background:#243;border:1px solid #5a7;color:#dde; font:13px ui-monospace,monospace;box-shadow:0 6px 16px #000b; @@ -572,24 +593,32 @@ fn move_section( // 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). - let targets: Vec = seat + // 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 .map(|seat| { legal .iter() - .filter_map(|c| crate::input::affordance(c, seat)) - .filter(|(f, _)| *f == action_id(a)) - .map(|(_, t)| t) + .filter_map(|c| { + crate::input::affordance(c, seat) + .filter(|(f, _)| *f == action_id(a)) + .map(|(_, t)| (t, crate::input::describe(c, seat))) + }) .collect() }) .unwrap_or_default(); + let targets: Vec = offered.iter().map(|(t, _)| t.clone()).collect(); + let descs: Vec = offered.iter().map(|(_, d)| d.clone()).collect(); if !targets.is_empty() { let _ = write!( s, "
{a:?}
\ + data-targets=\"{targets}\" data-descs=\"{descs}\">{a:?}
\ onto {names}
", id = action_id(a), targets = esc(&targets.join(" ")), + descs = esc(&descs.join("|")), names = esc(&target_names(&targets)), ); } diff --git a/crates/cb-render-html/src/input.rs b/crates/cb-render-html/src/input.rs index 8e2e604..6351740 100644 --- a/crates/cb-render-html/src/input.rs +++ b/crates/cb-render-html/src/input.rs @@ -96,6 +96,40 @@ pub fn affordance(command: &GroundCommand, seat: cb_kernel::PlayerId) -> Option< } } +/// What a drop would mean, in a sentence. +/// +/// **ADR-0010 Decision 1 puts this in Rust.** The page may render it; it +/// may not compose it. A script that assembled "Attack P2" from an id and +/// a seat name would be deriving a game fact, and would be wrong the +/// moment a command meant something the ids do not say. +pub fn describe(command: &GroundCommand, seat: cb_kernel::PlayerId) -> String { + let who = |p: cb_kernel::PlayerId| format!("P{}", p.0 + 1); + match command { + GroundCommand::SelectAction { + action, + target, + problem, + } => { + let head = match action { + Action::Attack => "Attack", + Action::Support => "Support", + Action::Solve => "Solve", + Action::Investigate => "Investigate", + Action::Ground => "Ground", + }; + match (target, problem) { + (Some(t), _) => format!("commit {head} against {}", who(*t)), + (_, Some(n)) => format!("commit {head} on problem {n}"), + _ => format!("commit {head}, untargeted"), + } + } + GroundCommand::SpendFreedom => { + format!("{} spends freedom to act again", who(seat)) + } + other => format!("{other:?}"), + } +} + /// Resolve a pointer fact to an index into the legal list. /// /// Returns `Err` rather than a default when nothing matches: a drag that diff --git a/crates/cb-render-html/src/jsrun.rs b/crates/cb-render-html/src/jsrun.rs index 5713ee3..4a84f60 100644 --- a/crates/cb-render-html/src/jsrun.rs +++ b/crates/cb-render-html/src/jsrun.rs @@ -64,13 +64,14 @@ var __all = []; // A DOM node with a real classList and real attributes. CB-WP-0016 found // that a stub too thin to express a failure is how the failure survives; // the previous stub could not express class-toggling at all. -function __mk(key, targets, text) { +function __mk(key, targets, text, descs) { var n = { parentNode: null, textContent: text || key, style: {}, _cls: {}, - _attr: { 'data-drop': key, 'data-targets': targets || null }, + _attr: { 'data-drop': key, 'data-targets': targets || null, + 'data-descs': descs || null }, getAttribute: function (a) { return this._attr[a] !== undefined ? this._attr[a] : null; }, setAttribute: function (a, v) { this._attr[a] = v; }, classList: { @@ -81,7 +82,7 @@ function __mk(key, targets, text) { }; return n; } -function __register(key, targets) { var n = __mk(key, targets); __all.push(n); return n; } +function __register(key, targets, descs) { var n = __mk(key, targets, key, descs); __all.push(n); return n; } var document = { body: __body, @@ -103,7 +104,9 @@ function fetch(url, opts) { // every one of them (CB-EV-0014 section 2). function __down(k) { __handlers['pointerdown']({ target: __find(k), clientX: 1, clientY: 2 }); } function __up(k) { __handlers['pointerup']({ target: __find(k), clientX: 3, clientY: 4 }); } -function __move() { if (__handlers['pointermove']) { __handlers['pointermove']({ clientX: 9, clientY: 9 }); } } +function __move() { if (__handlers['pointermove']) { __handlers['pointermove']({ clientX: 9, clientY: 9, target: __mk(null, null, "") }); } } +function __moveOver(k) { __handlers['pointermove']({ clientX: 5, clientY: 5, target: __find(k) }); } +function __ghostText() { return __body.children.length ? __body.children[0].textContent : ""; } function __cancel() { __handlers['pointercancel']({ target: __find(null) }); } function __find(k) { for (var i = 0; i < __all.length; i++) { @@ -230,12 +233,29 @@ pub fn prepare(ctx: &quick_js::Context, html: &str) -> Result<(), String> { Ok(()) } +/// The `data-descs` of the element carrying `key`, if any. +pub fn descriptions(html: &str, key: &str) -> Option { + let needle = format!("data-drop=\"{key}\""); + let i = html.find(&needle)?; + let tail = &html[i..]; + let end = tail.find('>')?; + let k = tail[..end].find("data-descs=\"")?; + let t = &tail[k + 12..]; + t.find('"').map(|e| t[..e].to_string()) +} + fn register(ctx: &quick_js::Context, html: &str) -> Result<(), String> { for (key, targets) in droppables(html) { - let call = match targets { - Some(t) => format!("__register({}, {});", json_lit(&key), json_lit(&t)), - None => format!("__register({}, null);", json_lit(&key)), - }; + // Descriptions are registered too: the stub could not express + // `data-descs` at all, so the first version of the description + // test failed against a DOM that simply did not carry them. + let descs = descriptions(html, &key); + let call = format!( + "__register({}, {}, {});", + json_lit(&key), + targets.map(|t| json_lit(&t)).unwrap_or("null".into()), + descs.map(|d| json_lit(&d)).unwrap_or("null".into()), + ); ctx.eval(&call) .map_err(|e| format!("register {key}: {e}"))?; } @@ -274,6 +294,15 @@ mod tests { target: Some(PlayerId(1)), problem: None, }, + // TWO attack targets on purpose. With one, an off-by-one in + // the target/description pairing is a no-op and the mutations + // that should catch it survive — a fixture too thin to express + // the failure, which is how the failure survives (CB-EV-0014). + GroundCommand::SelectAction { + action: Action::Attack, + target: Some(PlayerId(2)), + problem: None, + }, ]; crate::doc::document( &crate::testfix::view(Some(PlayerId(0))), @@ -536,6 +565,59 @@ mod tests { } } + /// **ADR-0010 D1 for descriptions.** Dragging over a legal target + /// shows the sentence Rust wrote **for that pair** — not one the page + /// assembled, and not a neighbouring pair's. + #[test] + fn dragging_over_a_target_shows_the_description_rust_wrote_for_it() { + let html = page(); + let ctx = ctx_for(&html); + + // What Rust wrote on the card, read back out of the document. + let card = droppables(&html) + .into_iter() + .find(|(k, _)| k == "action-attack") + .expect("the attack card"); + let targets = card.1.expect("targets"); + let descs = descriptions(&html, "action-attack").expect("descs"); + let i = targets + .split(' ') + .position(|t| t == "seat-1") + .expect("seat-1 is a target"); + let want = descs.split('|').nth(i).expect("a description").to_string(); + assert!(!want.is_empty(), "the description is blank"); + + ctx.eval("__down('action-attack'); __moveOver('seat-1');") + .expect("drag over"); + let shown: String = ctx.eval_as("__ghostText()").expect("ghost text"); + assert_eq!(shown, want); + + // Over nothing droppable it falls back to the label, rather than + // keeping a stale explanation pointing at the wrong element. + ctx.eval("__move();").expect("move away"); + let away: String = ctx.eval_as("__ghostText()").expect("ghost text"); + assert_ne!(away, want, "the explanation stuck after leaving the target"); + } + + /// Every advertised target has a description; a card offering three + /// drops and two sentences would silently mispair them. + #[test] + fn every_advertised_target_carries_its_own_description() { + let html = page(); + for (key, targets) in droppables(&html) { + let Some(targets) = targets else { continue }; + let descs = descriptions(&html, &key) + .unwrap_or_else(|| panic!("{key} advertises targets but no descriptions")); + assert_eq!( + targets.split(' ').count(), + descs.split('|').count(), + "{key}: {} target(s) but {} description(s)", + targets.split(' ').count(), + descs.split('|').count() + ); + } + } + #[test] fn both_script_blocks_are_extracted_in_order() { let found = scripts(&page()); diff --git a/evidence/CB-EV-0016-the-browser-is-a-client.md b/evidence/CB-EV-0016-the-browser-is-a-client.md new file mode 100644 index 0000000..d78a419 --- /dev/null +++ b/evidence/CB-EV-0016-the-browser-is-a-client.md @@ -0,0 +1,182 @@ +# CB-EV-0016 — the browser is a client, and window 1's verdict + +CB-WP-0018 T04. Measured 2026-08-03 at `e8bb726`+. Pass kind `product`, +tier **M** (structural M — changes the loop's own constraints; chaos d4=3, +no override). **Declaration 1 of chaos window 2**, opened by this pass. + +--- + +## 1. A finished game was indistinguishable from a crash + +Reported: *"after some time i get an empty page back. I guess the game +crashes or ends but that is unclear as the ui disappears."* + +Reproduced before touching anything, by driving a real game to completion +over HTTP: + +``` +move 5 accepted → ok +GET / → [Errno 111] Connection refused +``` + +The game had **ended normally** — 5 rounds, 30 commands — and its entire +result went to a terminal nobody was reading. `next_choice` only accepts +connections *inside* a human decision point, so when `play()` returned the +listener died and the page's post-`ok` reload was refused. + +**The browser was a second-class client.** The outcome, the scores, the +winners, and every error `run_game` can return were invisible to the only +interface a player uses. Now: an 8,998-byte page reading *"GROUND — game +over … 30 commands, hash f6c890a65271"*, with the final table and the log. + +It serves until the page posts `done` — the ending carries a *"close — I +have read this"* control — with a 600 s linger, because a server that +never exits is its own defect and a timeout would race a player reading +the result. + +## 2. The control had to be built twice, and the first was worthless + +`the_end_of_the_game_reaches_the_browser` calls `serve_end` directly. It +passes. **Deleting the call from `run_game` left it green.** + +That is CB-EV-0012's finding recurring almost verbatim — *"every link was +tested and the chain was not"* — and it survived one full round of +mutation here before anyone noticed, because the mutation was run and the +verdict was read as "no coverage gap" rather than "the test is in the +wrong place". + +`a_real_game_played_to_its_end_leaves_the_ending_on_screen` runs the real +`play()` with a browser seat, drives a real game to its end over a real +socket, and requires the last page to be the ending. Under the same +mutation it goes red — and its failure message prints an **empty page**, +which is precisely the symptom that was reported. + +**A weak assertion of mine, caught by itself.** The first version grepped +the ending page for `location.reload`. The page reuses `SCRIPT`, whose +reload is guarded by `t.indexOf('ok') === 0`, and the ending endpoint +answers `closed` — so the grep would have forced a second script into +existence to satisfy a test rather than a requirement. That is the +source-text control shape ADR-0010 D2 demoted three passes ago, reappearing +in my own hands. + +## 3. The card report, and what it actually was + +Reported: *"the cards I play by pulling them on a target will not be +removed … we will need a discard pile."* + +**A discard pile already existed** — `solution_discard` on `GroundState`, +`SolutionDiscarded` removing the card from the hand, and the page already +rendering `deck N remaining / discard …`. Building one would have been +building a thing that was there, and the only reason that did not happen is +that the code was read before the work started. + +What was being dragged are **action** cards. The five GROUND actions are +not cards and are correctly never consumed; solution cards leave the hand +at **Resolve**, because a selection is a face-down commit. + +But the report pointed at something real. Measured live: + +| move | hand | discard | +|---|---|---| +| 1 · Investigate → problem-2 | 2 cards | none | +| 2 · Investigate → problem-3 | **3 cards** | none | +| 3–5 · Solve → problem-1 | **4 cards, unchanged** | none | + +Investigate draws, correctly. **Solve was played three times and did +nothing, three times, in silence** — GR-A02's resolver `continue`s when the +problem is face-down, and `legal_commands` offers Solve on every face-up +problem without consulting the hand. + +**Raised for `ground-game`, not decided here** (INTENT defers game +semantics): *should SOLVE be selectable against a face-down problem, or +against a suit the seat cannot match?* A face-down commit you cannot +fulfil is a plausible bluff in a commit/reveal game with DARVO, which is +exactly why it is not this repo's call. + +## 4. The log, and the limit of what it can honestly say + +`bot::Journal` — `Applied { actor, command, events }` appended by the +driver through the new `play_journaled`; `play` delegates with `None`, so +nothing existing changed. `BotGame.events` is the same information but only +after `play` returns, which is no use to a page rendered mid-game. + +Phrased with `record::to_step`, so *what the player reads is what the +scenario file will say*, and all 29 `GroundEvent` variants render in words +rather than `{:?}`. + +**A command that produced no events says `no effect`**, and the mutation +removing that branch goes red. + +**The honest limit:** the reported SOLVE case does *not* render as `no +effect`, because the SOLVE resolves inside the system's `resolve` command, +which does produce events for other seats. The player now sees the +selection and sees no claim follow it — a large improvement on silence, but +still an inference. Making it explicit would require the renderer to decide +*why* a rule did nothing, which is a second implementation of the rules and +is what this task's own control forbids. Left as an inference deliberately. + +## 5. The explanation, and two mutations that a thin fixture defeated + +Every advertised target now carries a sentence Rust wrote for **that pair** +(`data-descs`, in step with `data-targets`), shown at the pointer while +dragging over it. ADR-0010 D1 binds: the page renders it, never composes +it. + +Both mutations — showing a neighbouring pair's text, and letting targets +and descriptions fall out of step — **initially survived**, because the +test fixture's Attack card had exactly **one** target, where an off-by-one +shift and a truncation are both no-ops. + +That is CB-EV-0014's lesson again, one level in: *a stub too thin to +express a failure is how the failure survives*. The fixture now offers two +attack targets on purpose, and both mutations go red. + +## 6. Window 1's verdict, and a rate change on n=2 + +Twelve declarations, two overrides, one each way, **both changed the +outcome** — so window 1's retirement condition was not met and the +mechanism is kept. The full table is in `InnerLoopReference.md` §Chaos +roll. + +**Rate dropped d4 → d8; window 2 opened at 12 declarations; new retirement +condition: retire if an override changes nothing twice running.** + +**The weakest part of this pass, stated plainly: it is a rate change argued +from n=2.** The alternative — keep d4 for a second window and decide with +four points — was live, and was rejected only because a quarter of all +declarations is a large standing tax to pay for evidence. So window 2 +carries a falsifier: **if it produces no override at all, that is evidence +the rate went too far**, not that the mechanism is healthy. A window that +cannot fire cannot be evaluated, which is the exact failure d10 had. + +## 7. Cost + +| pass | kind | responses | cost | $/response | +|---|---|---|---|---| +| CB-WP-0016 | product | 64 | $14.93 | 0.233 | +| **CB-WP-0017** | product | 40 | **$9.48** | 0.237 | +| CB-WP-0018 | product | *provisional — not quoted* | | | + +Read by **re-running `make status` at the moment of writing**, which is +CB-EV-0015 §6's correction applied for the first time: quoting a figure +remembered from earlier in a session defeats the rule even when the +boundary is right. CB-WP-0017 was reported at $5.19/23 mid-flight and +settled at **$9.48/40** — 83% higher. **Six for six, always low.** + +**Meta budget 0% `[ok]`**, all three trailing passes product. + +## 8. Open + +- **INTENT stage 1: the human check.** Three runs, three defects no test + could reach. Everything in this pass is verified by tests, mutation and a + live socket; nothing perceptual is. +- **For `ground-game`:** should SOLVE be selectable against a face-down + problem or an unmatchable suit? §3. +- **The self-quoting rule** now has both halves recorded but is still not + written into the loop spec. +- **AM-4b's scope defect (408,237 uncounted lines)** and its proc-macro + share. +- **`python3` as a toolchain dependency was never argued.** +- **AM-4a cannot survive stage 2** — 1,741,979 against 161,000. +- **ADR-0007 D3's acquisition rule** and **D5** remain unratified; + ADR-0010 rests on the latter. diff --git a/gates.toml b/gates.toml index 680c6d4..9fde325 100644 --- a/gates.toml +++ b/gates.toml @@ -120,15 +120,15 @@ retire_if = "two passes run with no finding while artifacts keep growing — tha id = "CHAOS" name = "the chaos roll" target = "" -checks = "d4 on each tier declaration, 12-declaration calibration window" +checks = "d8 on each tier declaration, 12-declaration calibration window (window 2, opened 2026-08-03; window 1 ran at d4)" added = "2026-07-30" -review_by = "2026-09-30" +review_by = "2026-11-30" caught = [ "CB-WP-0011: first fire in 6 declarations — d4=4 rolled stage 1 from structural L to S; the deleted survey would have opened on 2D toolkits while the existing text renderer was showing 24 of 41 view fields (CB-EV-0009 §1)", "CB-WP-0017: d4=4 — second override in twelve, and the first to roll UP (structural S → M). It bought ADR-0010: the script's widening from 'it does one thing' to holding a drag, following the pointer and marking other elements would otherwise have landed under a tier-S provenance paragraph, silently outgrowing ADR-0007 D5. The ADR's own finding is that the permitted and forbidden designs are indistinguishable from outside, which demoted the vocabulary grep to a cheap first line and produced the two behavioural controls that replaced it", "CB-WP-0012: d4=1, no override — and the contrast is the entry. Tier L at full weight deleted its own structural trigger: adversarial review withdrew the capability port the declaration was made to build (ADR-0007 D2), and corrected the survey's headline claim by 85x (128x -> 1.5x, CB-EV-0010 §2). Two passes on one subject at two tiers, priced: 0.123 $/response at L against 0.099 at S (CB-EV-0010 §5)", ] -retire_if = "the window closes with no overridden tier producing a different outcome than the argued one — the evaluation this window exists to make possible" +retire_if = "an override changes nothing twice running (window 2 condition, CB-WP-0018 T04). Window 1's condition — no override changing the outcome — was NOT met: both did, so the mechanism was kept and the rate dropped d4 → d8 instead" # VERDICT, CB-EV-0015 §5 (window closed 2026-08-02, 12 declarations, 2 overrides). # Not retired: both overrides changed the outcome. CB-WP-0011 (L→S) bought a # defect in the existing renderer that the deleted survey would have walked diff --git a/specs/InnerLoop.md b/specs/InnerLoop.md index c41965c..1de6699 100644 --- a/specs/InnerLoop.md +++ b/specs/InnerLoop.md @@ -120,17 +120,22 @@ are never skipped for code-producing work. | **M** | Survey and ADR merged into one document; review optional | Touches a canonical interface, adds/updates an external dependency, **or changes whether or how the loop constrains its own operation** — budgets, gates, review requirements, or these tier rules (v1.6, ADR-0006 D5) | | **S** | One provenance paragraph in the commit message | Everything else (utilities, fixes, refactors inside a boundary) | -**The chaos roll.** After deriving the structural tier, roll **d4** -(`shuf -i 1-4 -n 1`). On a **4**, the tier is instead picked uniformly at +**The chaos roll.** After deriving the structural tier, roll **d8** +(`shuf -i 1-8 -n 1`). On an **8**, the tier is instead picked uniformly at random (`shuf -e S M L -n 1`), overriding the structural derivation — up or down. -> **Calibration window, opened 2026-07-31, running to 12 tier -> declarations** (declaration 4 of 12 as of 2026-08-01). The rate was -> raised from d10 to d4 because at d10 the mechanism never fired and -> so prevented its own evaluation. Record the roll every time, -> including when it changes nothing (`tier: L (structural L, chaos 4)`). -> Rationale, cost estimate and the two dead rolls: +> **Window 1 closed 2026-08-02** at 12 declarations, 2 overrides, one each +> way, and **both changed the outcome** — so the mechanism was kept and +> the rate dropped d4 → d8 (CB-EV-0015 §5, CB-EV-0016 §4). +> +> **Window 2, opened 2026-08-03 at d8**, running to 12 declarations. +> Retirement condition: **retire if an override changes nothing twice +> running.** +> +> Record the roll every time, including when it changes nothing +> (`tier: L (structural L, chaos 8)`). Why the rate fell, why n=2 makes +> that the weakest part of the decision, and the dead rolls: > `specs/InnerLoopReference.md` §Chaos roll — calibration. Chaos limits: a rolled-down tier relaxes *process* weight only. Invariants diff --git a/specs/InnerLoopReference.md b/specs/InnerLoopReference.md index bf2b529..e60f773 100644 --- a/specs/InnerLoopReference.md +++ b/specs/InnerLoopReference.md @@ -195,3 +195,35 @@ four implementation rules the pass earned, and the requirement that evidence state what it does not support. Rationale and the failures behind each: `history/260731-inner-loop-retrospective.md`. + +## Chaos roll — window 1's verdict and the d4 → d8 change + +*(CB-WP-0018 T04, 2026-08-03. Full argument in `evidence/CB-EV-0015.md` §5 +and `evidence/CB-EV-0016.md` §4.)* + +Window 1 ran 2026-07-31 → 2026-08-02, twelve declarations, at d4 after an +earlier d10 that never fired and so prevented its own evaluation. + +**Two overrides, one in each direction, and both changed the outcome**, so +window 1's retirement condition — *"the window closes with no overridden +tier producing a different outcome than the argued one"* — was not met: + +| pass | roll | what the override bought | +|---|---|---| +| CB-WP-0011 | structural L → **S** | the deleted survey would have opened on 2D toolkits; the pass instead found the existing text renderer showing 24 of 41 view fields. Priced on the same subject: 0.099 $/response at S against 0.123 at L | +| CB-WP-0017 | structural S → **M** | ADR-0010. At tier S the page's script would have grown from *"it does one thing"* to holding a drag, following the pointer and marking other elements under a one-paragraph commit note, silently outgrowing ADR-0007 D5 | + +**Why the rate fell.** Both were informative *because they were rare*. At +d4 the mechanism overrides a quarter of all declarations, at which point it +stops being a calibration on the tier table and becomes a second tier +table. d8 keeps the mechanism and restores its rarity. + +**The weakest part of this decision, stated plainly:** it is a rate change +argued from **n=2**. The alternative — keep d4 for a second window and +decide with four data points — was live and was rejected only because a +quarter of declarations is a large standing tax to pay for evidence. + +So window 2 carries a falsifier: **if it produces no override at all, that +is evidence the rate went too far**, not evidence the mechanism is +healthy. A window that cannot fire cannot be evaluated, which is the exact +failure d10 had. diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index a0659bb..083c32f 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -19,7 +19,7 @@ use std::rc::Rc; use cb_game_runtime::{Project, Viewer}; use cb_kernel::PlayerId; -use cb_render_html::{document, resolve, Guard, PointerFact, Request}; +use cb_render_html::{resolve, Guard, PointerFact, Request}; use games_ground::bot::{Choice, Policy}; use games_ground::{GroundCommand, GroundState}; diff --git a/workplans/CB-WP-0018-the-browser-is-a-client.md b/workplans/CB-WP-0018-the-browser-is-a-client.md index 0955fed..e125086 100644 --- a/workplans/CB-WP-0018-the-browser-is-a-client.md +++ b/workplans/CB-WP-0018-the-browser-is-a-client.md @@ -2,7 +2,7 @@ id: CB-WP-0018 kind: product title: "The browser is a client: game over, a log, and where a drop goes" -status: todo +status: done --- # Purpose @@ -235,7 +235,7 @@ the question of whether the move should be offered at all. ```task id: CB-WP-0018-T03 -status: todo +status: done priority: high ``` @@ -257,11 +257,23 @@ description a target shows is the one Rust wrote for *that* pair — a mutation that shows a neighbouring pair's text must go red. The set equality property from ADR-0010 D2 must still hold. +**Done 2026-08-03.** `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 appears beside the target without an overlay +layer to keep aligned. + +**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. That is CB-EV-0014's lesson one level in: *a fixture too +thin to express a failure is how the failure survives*. The fixture now +offers two attack targets on purpose and both go red. + ## Task: close the chaos change, and the evidence ```task id: CB-WP-0018-T04 -status: todo +status: done priority: high ``` @@ -293,3 +305,21 @@ Then `evidence/CB-EV-0016-*.md`: memory — CB-EV-0015 §6 found that quoting a remembered figure defeats the rule even when the boundary is right, and this is the first pass that can apply that correction. + +**Done 2026-08-03.** +[CB-EV-0016](../evidence/CB-EV-0016-the-browser-is-a-client.md). `make +all` exits 0. + +- **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**, and window 2 therefore carries a falsifier: no override at + all is evidence the rate went too far. +- The rationale moved to `InnerLoopReference.md` because `InnerLoop.md` + hit 401 lines and the loadability gate fired — fixed 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 §6's correction applied + for the first time.