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.