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

136 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](../workplans/CB-WP-0017-legible-interaction.md),
[ADR-0007](ADR-0007-render-html-not-a-port.md) D5 (control 5),
[CB-EV-0014](../evidence/CB-EV-0014-the-drop-target.md) §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.