# CB-EV-0015 — legible interaction, and the chaos window's verdict CB-WP-0017 T04. Measured 2026-08-02 at `ca92db5`+. Pass kind `product`, tier **M** (structural S, **chaos d4=4 → OVERRIDE, drawn M**). **Declaration 12 of 12 — the chaos calibration window closes here.** --- ## 1. The page was lying, and now it is not The maintainer could drag after CB-WP-0016 but could not tell what was draggable, what was held, where it could go, or what any of it would do. Underneath that was something worse than a missing affordance: the page was **wrong about which moves exist**. Seat P1, round 1, step Select — **9 legal commands**, rendered as 5 cards each claiming all three target kinds. Before and after, from the live server: | card | before | after | |---|---|---| | Investigate | *"onto a seat, a problem, or the table"* | **onto problem 2, problem 3** | | Solve | *"onto a seat, a problem, or the table"* | **onto problem 1** | | Attack | *"onto a seat, a problem, or the table"* | **onto P2, P3** | | Support | *"onto a seat, a problem, or the table"* | **onto P2, P3** | | Ground | *"onto a seat, a problem, or the table"* | **onto the table** | Investigate is legal on problems 2 and 3 but **not** 1; Solve on 1 but not 2 or 3. The old text was a `const` in the emitter, printed whenever *any* legal command used that action. `legal` carried the exact targets and the renderer threw them away. So drop-target highlighting is not decoration on a working table. It is the first time the page tells the truth about what is legal. ## 2. ADR-0010: the constraint that replaced "it does one thing" ADR-0007 D5 said the page *"reports raw pointer facts and nothing else"* and `doc.rs` said `SCRIPT` *"does one thing"*. Everything asked for here needs it to do more — hold a drag, follow the pointer, mark other elements. Restating a control in order to permit what it forbade is how controls die, so it was restated out loud: > **Every game fact the page acts on must arrive from Rust as data.** The > script may read it, match it, and turn it into presentation. It may not > compute, infer, filter, or default one. The survey's real finding: the permitted design (Rust emits legality, the script matches it) and the forbidden one (the script derives legality from the DOM) are **indistinguishable from the outside** — both are "the page highlights some elements". A control that cannot separate them is not a control. The old control was a grep of `SCRIPT` for game vocabulary. CB-WP-0014 already found that shape too weak once. It is now demoted to a cheap first line, and the load-bearing control is stated over behaviour: **Property 1 — the highlighted set equals the set Rust emitted.** The mutation that makes the script derive targets by pattern-matching ids produces a *plausible* highlight — `seat-0, seat-1, seat-2` where only `seat-1` is legal — and the test catches it. That is Decision 1 actually enforced rather than asserted. **Property 2 — a target the page marks legal must `resolve`.** A page that advertises a drop Rust then refuses is worse than no highlighting, because it teaches the player something false. Mutation: adding `table` to Investigate's targets → *"the page advertises action-investigate -> table but resolve refuses it"*. ## 3. What is visible now, and what is merely tested | request | done | verified how | |---|---|---| | what can be picked up | `.pick` — shadow, lift on hover, `grab` cursor | **not verified.** Whether it *reads* as pickable is a human check | | what is held | `.held` — dimmed and scaled where it sat | JS test: `__heldKeys()` is exactly the grabbed key | | where it can go | `.dropok` on every legal target, **both drawings of a seat** | JS test: set equality with `data-targets` | | where it is going | a ghost following the pointer | JS test: ghost exists, and its `left` tracks `clientX` | | what moves exist | the card names its real targets | live server, §1 | **A seat is marked on both of its drawings** — card and graph node. That is asserted rather than tolerated: highlighting only one would send the player back to hunting for the one that works, which is the CB-WP-0016 defect in a new form. The first draft of that assertion was *wrong* about it, expecting `seat-1` and reading `seat-1,seat-1`. **Third mutation:** dropping the `pointercancel` handler and the `clear()` on pointerup leaves marks behind — *"marks survived `__up('seat-1')`"*. A stale highlight set is worse than none. ### What this pass does not claim No visual property is verified. Whether the shadow reads as *"pick me up"*, whether the highlight is noticeable, whether the drag feels followable — none of it is reachable from here, and ADR-0010 D5 says so rather than letting the tests imply otherwise. ## 4. The harness had to get deeper, and cost an hour to a real trap The DOM stub could not express any of this: it synthesized `{target:{id}}` from whatever key the test passed. It now models `classList`, `querySelectorAll`, `createElement`, and a body — **and its node set is built from the real emitted page** via `droppables(html)`, not from a list the harness invented. **The trap, recorded because it will recur:** QuickJS fixes its stack limit at `Context` creation *relative to the frame it was created in*. A helper that creates a context and returns it leaves every later `eval` running in a shallower frame, the comparison underflows, and QuickJS reports `SyntaxError: stack overflow` — for `__all.length`. The fix is that the caller owns the `Context` and the helper only prepares it. That is also why one test had silently skipped registration: each test hand-rolled its setup. `prepare()` is now shared, so a test cannot set up a different page than the harness runs. ## 5. The chaos calibration window: the verdict it has owed `gates.toml` states the retirement condition in its own words: > *"the window closes with no overridden tier producing a different outcome > than the argued one — the evaluation this window exists to make > possible"* Twelve declarations, **two overrides**, one in each direction. That is the minimum that makes an evaluation possible at all, and it arrived on the last declaration. ### Override 1 — CB-WP-0011, structural L → S The deleted survey would have opened on 2D toolkits. What the pass found instead, at tier S, was that the **existing** text renderer was showing 24 of 41 view fields (CB-EV-0009 §1) — a defect a toolkit survey would have walked straight past. Priced on the same subject: **0.099 $/response at S against 0.123 at L** (CB-EV-0010 §5). **Different outcome than the argued one: yes.** The argued tier would have bought a survey of alternatives; the rolled tier bought a defect in what already existed. ### Override 2 — CB-WP-0017, structural S → M The question the retirement condition asks: **what did the extra ADR buy that a tier-S provenance paragraph would not have?** It bought the restatement of control 5. At tier S this pass would have widened what the JavaScript does — pointermove, class-toggling on other elements, a ghost node — under a one-paragraph commit note, and ADR-0007 D5's *"reports raw pointer facts and nothing else"* would have been silently outgrown rather than replaced. The forced document is what produced Decision 1's rule and, more importantly, the finding that the permitted and forbidden designs are indistinguishable from outside — which is what turned the vocabulary grep from *the* control into a cheap first line, and produced the two properties that are now the real controls. **Different outcome than the argued one: yes**, and in the direction that is harder to see from inside — the extra process caught a control quietly expiring. ### Verdict: keep, at a lower rate Both overrides changed the outcome, so the retirement condition is not met. But the honest reading of the cost is that d4 fired twice in twelve and both were informative *because they were rare*; a mechanism that overrides a quarter of all declarations stops being a calibration and starts being the tier system. **Recommendation: keep the chaos roll, drop the rate from d4 to d8, and open a second window of 12** with a new retirement condition — retire if an override changes nothing twice running. This is a change to how the loop constrains its own operation and is therefore **tier M work that this pass does not do**: it is recorded here as the recommendation and owed to the next declaration, which must make it with its own tier and its own roll. ## 6. Cost | pass | kind | responses | cost | $/response | |---|---|---|---|---| | **CB-WP-0016** | product | 64 | **$14.93** | 0.233 | | CB-WP-0017 | product | *provisional — not quoted* | | | Read from `make status` **at the moment of writing**, after CB-WP-0017's declaration closed CB-WP-0016's window. ### The self-quoting rule failed a second way, and CB-EV-0014 is corrected CB-EV-0014 §5 quoted CB-WP-0015 at **$15.14 / 136** and claimed it was *"the first figure this project has quoted at a boundary that had actually settled."* Both parts were wrong. The same tool now reports CB-WP-0015 at **$22.70 / 166** — higher by 50%. CB-EV-0014 has been corrected in place. $15.14/136 was read during CB-WP-0015 itself and carried forward into the next pass's write-up. So there are **two** defects, not one: 1. the boundary is wrong — a window runs to the *next* pass's first commit (CB-EV-0013 §5); 2. **a figure read earlier in a session is an in-flight figure regardless of whose name is on it.** Quoting from memory defeats the rule even when the boundary is right. Five for five now, always low: every self-reported cost in this project has been an underestimate. The rule owed is therefore not just *"quote two passes back"* but *"re-run the instrument at the moment you quote it"* — and the second half is what this file did. **Meta budget: 0% `[ok]`** over the trailing three, all product. ## 7. Open - **INTENT stage 1: the human check, again.** Everything perceptual here is unverified by construction. `cb-play --serve 0`: can you see what is pickable, what you are holding, and where it may go? - **Chaos: the d4→d8 recommendation** and a second window. §5. - **The self-quoting rule still names the wrong boundary.** CB-EV-0013 §5. - **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** unratified after deciding two dependency questions; **ADR-0010** now rests on D5, which is also unratified.