clay-borg/decisions/ADR-0010-what-the-script-may-do.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

6.1 KiB
Raw Blame History

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:

  1. 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.
  2. 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, down and up, 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 classList and 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.