Declare CB-WP-0016: the drop target that was never there, tier S

Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check CB-EV-0012 kept stage 1 open on was run by the maintainer and
found the drag broken. Diagnosed against the live server first:
down=action-attack&up=seat-1 returns ok, so socket, guard, resolve and
dispatch are correct. seat-{n} ids exist only on the SVG circles in the
relationship graph; player_card emits the visible seat cards with no id,
so the target every action card names is inert. jsrun feeds element ids
straight in and never hit-tests, which is why every test passed.
Structural tier S: a defect fix inside an existing capability, and the
check it adds is a product test rather than a control gate. Chaos d4=3,
no override. Declaration 11 of 12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-02 20:35:18 +02:00
parent d11e02e32b
commit 4df2d0a4ae

View file

@ -0,0 +1,136 @@
---
id: CB-WP-0016
kind: product
title: "The drop target that was never there"
status: todo
---
# Purpose
```
structural tier S (a defect fix inside an existing capability — no new
port, no canonical interface, no new dependency, and
the check it adds is a product test, not a control
gate)
chaos d4 = 3 → no override
declared tier S
```
Declaration 11 of 12. Tier S: one provenance paragraph in the commit
message, no survey and no ADR. Steps 45 are not skipped.
## Found by the human check, and by nothing else
CB-EV-0012 §4 kept INTENT stage 1 open on one action the loop could not
perform: run `cb-play --serve 0`, open the URL, confirm the table reads and
a drag works. The maintainer ran it on 2026-08-02. **The table reads. The
drag does not.**
Diagnosed against the running server before any code was changed:
```
POST down=action-attack&up=seat-1 → ok (the game advanced)
POST down=action-attack&up=action-attack → "not a legal move here"
```
So the socket, the token guard, `resolve`, and command dispatch are all
correct. The defect is in the emitted page.
**`seat-{n}` ids exist only on the ~26 px circles inside the relationship
graph** (`<g id="seat-0">`, `doc.rs:185`). The visible *seats* section
cards are emitted by `player_card` as `<div class="card" data-viewer=…>`
with **no id**. Every action card says *"drag Attack onto a seat, a
problem, or the table"*, and the seat it points at is not a target.
`#table`, `problem-{n}` and `freedom-{n}` are real targets, so the page is
not wholly inert — the most natural target is.
## Why every test in the repo passed
`jsrun::gesture(html, down, up)` calls `__down(id)`, which synthesizes
`{ target: { id: id } }`. **It feeds element ids straight in and never
hit-tests.** So it establishes *"the script posts the ids it was given"*
and never *"the page has an identified element where the instruction says
to drop."*
The 42-path coverage gate is blind for a related reason: it asserts each
view field is **present in the parsed document**, which a `<div>` with no
id satisfies perfectly.
CB-WP-0015 closed a clause whose test was named for a property it did not
check. This is the same shape one layer out — a harness answering a
narrower question than its name implies — and it is the argument for
having kept the human check as a blocker rather than closing stage 1 on
green tests two passes ago.
## Task: make the seat a drop target, and keep ids unique
```task
id: CB-WP-0016-T01
status: todo
priority: high
```
The seat card must carry the id `affordance()` already names for it.
`seat-{n}` is currently taken by the SVG node, and duplicate ids are
invalid HTML — so one of the two has to change, and **both must remain
droppable.** The graph circle works today and someone will have learned it.
Decide where the aliasing lives. Rust decides what a pointer fact means
(ADR-0007 D5), so widening what `resolve` accepts is in bounds and
inventing a new id vocabulary in the page is not.
**Control:** dropping on the seat *card* and dropping on the graph *node*
must produce the same command, and a test must go red if either stops
resolving.
## Task: a gesture that lands nowhere must say so
```task
id: CB-WP-0016-T02
status: todo
priority: high
```
`SCRIPT` currently does `if (!down || !up) { down = null; return; }` — no
POST, no status line, nothing. That silence is why this defect survived a
human sitting in front of it.
The Rust side already holds the right principle: `resolve` refuses rather
than substituting a default, because *"a drag that means nothing must mean
nothing, not the first legal move."* Refusing is correct. Refusing
**silently** is not.
Make the outcome visible. Keep ADR-0007 control 5 intact — whatever the
page reports must still be raw pointer facts with no game vocabulary, and
the existing body-shape assertion must still hold.
## Task: the check that closes the class
```task
id: CB-WP-0016-T03
status: todo
priority: high
```
**For every legal command, both ids `affordance()` produces must occur in
the emitted document.** Pure Rust, no JS engine, no browser, deterministic.
It fails on today's tree and it would have caught this the day the
renderer was written.
This is the deliverable that matters — T01 fixes one instance, this closes
the class. An affordance naming an element that does not exist is exactly
the harness-does-nothing shape, and it currently has no detector at all.
Then `evidence/CB-EV-0014-*.md`:
- **What the human check bought**, priced honestly against what it cost to
keep stage 1 open for two passes.
- **Whether stage 1 closes now.** It needs the maintainer to drag again;
say so rather than closing it on a passing test, which is the failure
mode this whole pass is evidence for.
- **Quote CB-WP-0015's final cost, not this pass's** — and note that
CB-EV-0013 §5 found the self-quoting rule fixes the wrong boundary, so
say plainly whether the figure has settled.
- **Chaos: declaration 11 of 12.** The calibration window closes next
declaration.