clay-borg/evidence/CB-EV-0015-legible-interaction.md
tegwick a55e878bf0
Some checks failed
ci / check (push) Failing after 4s
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

10 KiB

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.