137 lines
6.1 KiB
Markdown
137 lines
6.1 KiB
Markdown
|
|
# 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.
|