clay-borg/workplans/CB-WP-0017-legible-interaction.md
tegwick 332d8f1bba
Some checks failed
ci / check (push) Failing after 3s
Sync hub IDs and work-record index for CB-WP-0017
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:43:40 +02:00

262 lines
11 KiB
Markdown

---
id: CB-WP-0017
kind: product
title: "Legible interaction: what can be picked up, what is being dragged, where it can go"
status: done
state_hub_workstream_id: "e5080127-8cde-40de-bf03-2898a98491ca"
---
# Purpose
```
structural tier S (presentation work inside an existing capability —
no new port, no canonical interface, no new
dependency, no change to the loop's own constraints)
chaos d4 = 4 → OVERRIDE, tier drawn: M
declared tier M (structural S, chaos 4 → M)
```
**Declaration 12 of 12 — the chaos calibration window closes here**, and
owes its evaluation (T04).
This is the **second override in twelve declarations**, and it rolls the
opposite way from the first: CB-WP-0011 was structural L rolled *down* to
S, this is structural S rolled *up* to M. The window therefore has one of
each to evaluate, which is the minimum that makes an evaluation possible
at all.
Tier M means survey and decision merge into one document and the
adversarial review is optional.
## The report this pass exists for
The maintainer ran stage 1's human check again after CB-WP-0016 and could
drag — but:
> *"it is not possible to get what moves are possible and how they affect
> the state of the game"* … *"Can i have better visualization of which
> Elements in the ui can be manipulated"* … *"If the element isnt shown as
> picked up, there is no way to understand that i am actually dragging it
> or where i am dragging it"* … *"visual clues about where something can be
> dropped when it is picked up"*
Four requests, and one prior finding that makes them one pass: **the page
already hides which moves are legal.**
Measured, seat P1 at round 1 step Select — **9 legal commands**, rendered
as 5 cards each claiming all three target kinds:
| the card says | actually legal |
|---|---|
| Investigate → *"a seat, a problem, or the table"* | **problem-2, problem-3** |
| Solve → *"a seat, a problem, or the table"* | **problem-1** |
| Attack → *"a seat, a problem, or the table"* | **seat-1, seat-2** |
| Support → *"a seat, a problem, or the table"* | **seat-1, seat-2** |
| Ground → *"a seat, a problem, or the table"* | **table** |
Investigate is legal on problems 2 and 3 but not 1; Solve on 1 but not 2
or 3. The card text is a **constant**`"drag {a:?} onto a seat, a
problem, or the table"` — emitted whenever *any* legal command uses that
action, then naming all three kinds regardless. `legal` carries the exact
targets and the renderer discards them.
So highlighting drop targets is not decoration on top of a working table.
It is the first time the page will tell the truth about what is legal.
## Task: decide how far the JavaScript may grow
```task
id: CB-WP-0017-T01
status: done
priority: high
state_hub_task_id: "be0f8249-51b2-4fef-89c0-9fad660563b4"
```
Write `decisions/ADR-0010-*.md` (tier M: survey and decision in one).
**This is a real decision, not a formality.** ADR-0007 Decision 5 says 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"*.
Everything asked for here needs the script to do **more** than one thing —
track a drag in flight, move a ghost with the pointer, add and remove
classes on other elements.
Decide, explicitly:
- **Does class-toggling violate control 5?** The honest argument is that
legality still comes from Rust as data and the script only renders it —
but that argument must be *made*, because "the page renders what Rust
said" is exactly what a page constructing commands would also claim.
- **What is the invariant that replaces "it does one thing"?** If the
script may grow, say what it may never do, in a form a test can check.
The existing no-game-vocabulary test is the candidate and it is weak.
- **The cost.** Marginal AM-4a is zero either way, so the budget does not
decide this. Say what does.
**Done 2026-08-02.**
[ADR-0010](../decisions/ADR-0010-what-the-script-may-do.md) — *the script
may render state it was given; it may not derive any.*
The survey's real output: 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". So the vocabulary grep is demoted to a cheap
first line and the load-bearing controls are stated over behaviour: the
highlighted set must **equal** the set Rust emitted, and anything the page
marks legal must **resolve**. Both are mutation-proven below.
## Task: tell the truth about which moves are legal
```task
id: CB-WP-0017-T02
status: done
priority: high
state_hub_task_id: "749e03f3-70b6-458d-be9c-4a178808af9e"
```
Emit the legal targets **derived from `legal`**, per action, and render
them so the table is legible without dragging anything.
The generic sentence goes. A card that says *"onto a seat, a problem, or
the table"* when only `problem-1` is legal is not a simplification, it is
wrong.
**Controls:**
- The emitted target set must equal the affordance targets of the legal
list exactly — pure Rust, no browser, and it must go red when the sets
diverge.
- A drop that the page marked legal must **resolve**. If the page can
advertise a target `resolve` then refuses, the two have drifted and the
highlighting is worse than none.
**Done 2026-08-02.** From the live server: *"Solve onto problem 1"*,
*"Investigate onto problem 2, problem 3"*, *"Attack onto P2, P3"*,
*"Ground onto the table"*. The constant sentence is gone.
`everything_the_page_advertises_actually_resolves` drives four real bot
games and checks every emitted target against `resolve` itself. Mutation —
adding `table` to Investigate's targets — goes red with *"the page
advertises action-investigate -> table but resolve refuses it"*.
## Task: make picking up, dragging, and dropping visible
```task
id: CB-WP-0017-T03
status: done
priority: high
state_hub_task_id: "e607e4e1-3270-458d-9c49-be77d48cff0a"
```
The four requests, in order of how badly they are needed:
1. **What can be manipulated at all** — a resting affordance on every
pickable element. The maintainer asked for a slight shadow; the point
is that interactive and inert must not look identical.
2. **What is picked up** — the grabbed element must visibly change while
the pointer is down.
3. **Where it can go** — every legal drop target for *the thing currently
held* highlights, and nothing else does.
4. **Where it is going** — the drag must be followable, not invisible.
**Be honest about what is testable.** Whether a shadow reads as "you can
pick this up" is not something any test here can settle, and claiming
otherwise would be this project's characteristic error in its newest
costume. What *is* testable, and must be:
- pointerdown marks the grabbed element and **exactly** the legal targets
for it, and nothing else;
- pointerup and cancellation clear every mark — a drag that ends leaves no
residue, or the next drag highlights a stale set;
- the highlighted set is the same set T02 emitted.
The JS DOM stub cannot express any of this today. It will have to model
`classList` and element lookup, which is a real deepening of the harness —
and CB-WP-0016 showed that a stub too thin to express a failure is how the
failure survives.
**Done 2026-08-02.** `.pick` (resting shadow, lift on hover), `.held`
(dimmed and scaled), `.dropok` (dashed outline on every legal target), and
a ghost following the pointer.
The stub now models `classList`, `querySelectorAll`, `createElement` and a
body, **and its node set is built from the real emitted page** via
`droppables(html)` rather than from a list the harness invented.
Three mutations, each red:
| mutation | result |
|---|---|
| the script *derives* targets from the DOM instead of reading `data-targets` | red — marks `seat-0, seat-1, seat-2` where only `seat-1` is legal. **A plausible-looking highlight, caught.** This is ADR-0010 D1 enforced |
| the page advertises a target `resolve` refuses | red |
| `pointercancel` and the `clear()` on pointerup are dropped | red — *"marks survived `__up('seat-1')`"* |
**A seat highlights on both of its drawings** — card and graph node — and
that is asserted rather than tolerated. The first draft of the assertion
was wrong about it: it expected `seat-1` and read `seat-1,seat-1`.
**A real trap, recorded:** QuickJS fixes its stack limit at `Context`
creation relative to the frame it was created in, so a helper that creates
a context and *returns* it makes every later `eval` report `SyntaxError:
stack overflow` — for `__all.length`. The caller must own the `Context`.
**Nothing perceptual is verified.** Whether a shadow reads as "pick me up"
is not reachable from here (ADR-0010 D5).
## Task: evidence, and close the chaos calibration window
```task
id: CB-WP-0017-T04
status: done
priority: high
state_hub_task_id: "b67442a5-ad27-495c-857a-b4ebe28b07bf"
```
`evidence/CB-EV-0015-*.md`, and then **the evaluation the window has owed
since 2026-07-31**.
`gates.toml` states the retirement condition in its own words:
> *retire_if = "the window closes with no overridden tier producing a
> different outcome than the argued one — the evaluation this window
> exists to make possible"*
So answer it, on the two overrides actually observed:
- **CB-WP-0011**, structural L → S. What did the deleted survey cost or
save? CB-EV-0009 §1 and CB-EV-0010 §5 already priced part of this
(0.123 $/response at L against 0.099 at S on the same subject).
- **CB-WP-0017**, structural S → M. What did the *extra* ADR buy that a
tier-S provenance paragraph would not have? If the answer is nothing,
say so — that is the finding the window was opened to produce, and a
mechanism that survives its own evaluation by being graded generously is
worse than one that is retired.
Then: keep, retire, or change the rate, with the argument.
Also due:
- **Whether stage 1 closes.** It needs a human to drag again. Say so.
- **Quote CB-WP-0016's cost**, and apply CB-EV-0013 §5's correction —
quote a boundary that has actually settled, or say it has not.
**Done 2026-08-02.**
[CB-EV-0015](../evidence/CB-EV-0015-legible-interaction.md). `make all`
exits 0.
- **The chaos window's verdict: keep, at a lower rate.** Both overrides
changed the outcome, so the retirement condition is not met. CB-WP-0011
(L→S) bought a defect in the existing renderer that a toolkit survey
would have walked past, at 0.099 $/response against 0.123. CB-WP-0017
(S→M) bought the restatement of control 5 — at tier S the script would
have outgrown ADR-0007 D5 under a one-paragraph commit note.
**Recommendation: d4 → d8, second window of 12, retire if an override
changes nothing twice running.** That is a change to how the loop
constrains its own operation, so it is tier-M work this pass does not
do — it is owed to the next declaration.
- **CB-EV-0014 is corrected.** It quoted CB-WP-0015 at $15.14/136 and
claimed that was the first settled figure this project had quoted. Both
wrong: it now reads **$22.70/166**. The number had been read during
CB-WP-0015 itself and carried forward, so there are two defects — the
boundary, and quoting a figure from memory rather than re-running the
instrument. Five for five, always low.
- **Stage 1 stays open**, and now on a purely perceptual question.