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>
6.1 KiB
ADR-0010: the script may render state it was given; it may not derive any
status: accepted date: 2026-08-02 decided by: agent, under the standing loop authorization tier: M (structural S — presentation work inside an existing capability; chaos d4=4 → OVERRIDE, drawn M. InnerLoop v1.6). Tier M merges survey and decision into one document, which this is. references: CB-WP-0017, ADR-0007 D5 (control 5), CB-EV-0014 §4
Context
ADR-0007 Decision 5 constrains the page:
the page reports raw pointer facts and nothing else, and Rust decides what command they mean.
and doc.rs says of SCRIPT: "it does one thing". Every request in
CB-WP-0017 needs it to do more than one thing — hold a drag in flight,
follow the pointer, and add and remove classes on other elements.
So the constraint has to be restated or the work does not happen. Restating a control in order to permit the thing it forbade is how controls die, and this ADR exists so that happens out loud or not at all.
The survey: three ways to make interaction legible
| option | where legality is decided | script size |
|---|---|---|
| A. Rust emits legality as data; the script toggles classes | Rust | ~2× today |
| B. The script derives legality from the rendered page | the page | ~3× |
| C. Server round-trip on pointerdown to ask what is legal | Rust | ~1.5×, plus a request per grab |
B is the one control 5 exists to forbid, and it is not hypothetical: the page already contains every action id and every target id, so a script could infer plausible pairs. It would be wrong in exactly the cases that matter — Investigate is legal on problems 2 and 3 but not 1, and no amount of looking at the DOM reveals why.
C is honest and too slow. A highlight that arrives after a network round trip is not a highlight; it also makes a pointerdown a state-changing request, which the token guard's threat model did not consider.
A is the decision. But the survey's real output is that A and B are indistinguishable from the outside: both are "the page highlights some elements". A control that cannot tell them apart is not a control.
Decision 1 — the script may render state it was given; it may not derive any
The replacement for "it does one thing":
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.
Under this rule, class-toggling is permitted: the legal target set is a
data-targets attribute Rust wrote, and the script does string matching on
it. Deriving that set by pattern-matching ids would be forbidden even
though the visible result is identical.
Decision 2 — the honest argument, and why the old test was weak
The tempting defence is "the page only renders what Rust said". That is exactly what a page constructing commands would also say, so it earns nothing on its own.
The existing control is a test that greps SCRIPT for game vocabulary.
CB-WP-0014 already found that shape too weak once — it was replaced for
control 5 by an assertion on what the script puts on the wire. The same
weakness applies here and is now worse, because the script legitimately
handles more.
So the vocabulary grep stays as a cheap first line, and the load-bearing control becomes a pair of properties a test can check:
- The highlighted set equals the set Rust emitted. Not a subset, not a superset. A script that derived legality would drift from the attribute the moment the two disagreed, and this catches that with no reference to how the script is written.
- A target the page marks legal must
resolve. If the page can advertise a drop that Rust then refuses, the two have drifted and the highlighting is worse than none — it would teach the player something false.
Property 1 is the real replacement for the grep. It is stated over observable behaviour rather than over source text, which is the same move that fixed control 5.
Decision 3 — what the script may still never do
Unchanged from ADR-0007 D5, and restated because this ADR widens the rest:
- it may not name an action, a seat, a rule, or a phase;
- it may not construct a command, in any encoding;
- what it posts stays exactly two fields,
downandup, carrying two element keys and nothing derived from them.
The body-shape assertion from CB-WP-0014 continues to hold this, and nothing in this ADR touches it.
Decision 4 — the size of the script is not the control
SCRIPT roughly doubles. That is not a violation of anything: ADR-0007's
concern was rules leaking into the page, not line count, and it said so —
"if a rule ever needs to appear in it, the decision is wrong". Marginal
AM-4a cost is zero either way, so the dependency budget does not decide
this.
What decides it is Decision 1's rule plus the two properties in Decision 2. Recording this explicitly so that a future pass does not cite the growth as either a licence or an objection.
Decision 5 — what this ADR does not claim
No visual property here is verified. Whether a shadow reads as "you can pick this up", whether a highlight is noticeable, whether a drag feels followable — none of that is reachable by any test in this repo, and CB-WP-0016 is the standing evidence for what happens when that gap is papered over.
The testable core is the correspondence between what the page marks and what Rust will accept. The perceptual half stays a human check, and stage 1 stays open on it.
Consequences
- The JS DOM stub must model
classListand element lookup. A stub too thin to express a failure is how the failure survives (CB-EV-0014 §2), and today's stub cannot express any of this. - The vocabulary grep is now explicitly the weak control, kept for cost. If it ever fails while property 1 passes, prefer property 1 and say so.
- If a later pass needs the script to make a decision Rust cannot pre-empt — an animation that depends on rules, say — Decision 1 forbids it and this ADR is what to revisit.