clay-borg/evidence/CB-EV-0014-the-drop-target.md
tegwick bf72a1863a
Some checks failed
ci / check (push) Failing after 4s
CB-WP-0016: the drop target that was never there
Provenance (tier S, one paragraph in lieu of survey and ADR): the human
check that kept INTENT stage 1 open was run and the drag was broken.
Root cause, worth more than the instance: drop targets were ids, and an
id must be unique, so exactly one element could ever be seat-0. The
relationship-graph circle took it and the seat card that every action
card's own text points at -- 'drag Attack onto a seat' -- silently had
none. A seat is drawn twice and both drawings are the seat; the document
model could not express that.

Drop keys are now data-drop. Any number of elements may carry the same
key, so a seat is droppable on its card and on its graph node. Measured
on a live server: seat-0/1/2 each appear twice, id survives only on
cb-status which is the one element the script looks up, and
down=action-attack&up=seat-1 returns ok.

Second defect: a drop on nothing returned without posting and without
touching the status line, so a broken target was indistinguishable from
a working page. resolve already refuses rather than defaulting, which is
right; refusing SILENTLY is not. The page now reports the raw fact --
'took action-attack, let go over nothing droppable' -- which names
elements, not moves, so ADR-0007 control 5 holds.

And the honest part: the general check added here -- every offered
affordance names a key that exists, driven through Policy::choose over
four real bot games -- does NOT catch the reported defect. seat-0 did
exist, on the graph circle. It is kept because a wholly absent target is
a real class, and paired with a targeted regression test that does catch
it. Three mutations, each red for its stated reason, including the
reported defect reintroduced; only the targeted test fires on that one.

A cb-play assertion matched id="action-ground" as a substring while
describing itself as checking the page; rewritten through drop_keys.

make all exits 0. Stage 1 stays open: verified by tests, mutation and a
live server, not by a human dragging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:48:18 +02:00

139 lines
6.6 KiB
Markdown

# CB-EV-0014 — what the human check bought
CB-WP-0016 T03. Measured 2026-08-02 at `4df2d0a`+. Pass kind `product`,
tier **S** (chaos d4=3, no override). Declaration 11 of 12.
Cost quotes **CB-WP-0015's** figure, per CB-EV-0012's rule and with the
correction CB-EV-0013 §5 attached to it. See §5.
---
## 1. The check found a defect every test in the repo was blind to
CB-EV-0012 §4 kept INTENT stage 1 open on one action the loop could not
perform. The maintainer ran it. **The table reads. The drag did not work.**
Diagnosed against the live server *before* any code changed, which is what
made the rest cheap:
```
POST down=action-attack&up=seat-1 → ok (the game advanced)
POST down=action-attack&up=action-attack → "not a legal move here"
```
Socket, token guard, `resolve` and dispatch: all correct. The defect was in
the page, and it had a root cause worth more than the instance:
> **Drop targets were `id`s, and an `id` must be unique.** So exactly one
> element could ever be `seat-0`. The relationship-graph circle took it,
> and the seat *card* — which every action card's own text points at,
> *"drag Attack onto a seat…"* — silently had none.
A seat is drawn twice and both drawings are the seat. The document model
could not express that.
## 2. Why nothing caught it, stated precisely
| control | why it was blind |
|---|---|
| `jsrun::gesture` | calls `__down(id)`, which synthesized `{target:{id}}`. It feeds element ids straight in and **never hit-tests** — it establishes *"the script posts the ids it was given"*, never *"there is an element there to give"* |
| the 42-path coverage gate | asserts each view field is present in the **parsed document**. A `<div>` with no id satisfies that perfectly |
| `resolve`'s unit tests | test the mapping from a fact to a command. The fact never arrives |
| M-D1-MUT | its population is the AM-* acceptance rows. None of them is about the page |
Every one was green. This is the shape CB-WP-0015 closed one layer in — a
harness answering a narrower question than its name implies — recurring in
the presentation layer.
## 3. The fix, and the check that is honestly insufficient
**Drop keys are now `data-drop`, not `id`.** Any number of elements may
carry the same key, so a seat is droppable on its card *and* on its graph
node. Measured on the live page: `seat-0`, `seat-1`, `seat-2` each appear
**twice**; `id` survives on exactly one element, `cb-status`, which is the
only one the script looks up.
Two checks, and the difference between them is the finding:
| check | catches the reported defect? |
|---|---|
| **every offered affordance names a key that exists** — drives four real bot games through `Policy::choose`, renders at every real decision point | **NO.** `seat-0` *did* exist, on the graph circle |
| **every seat card is a drop target, not only the graph node** | **yes** |
The general check is worth having — it fails when a target is wholly
absent, which is a real class — but **it would not have found the bug the
maintainer found**, and saying otherwise would be the exact error this
project keeps catching. An existence check over a whole document cannot
tell you the element the user is being *pointed at* is the one that works.
Hooking `Policy::choose` rather than re-driving the game by hand matters:
those are the same decision points `cb-play --serve` renders at, with the
same `legal` list. A hand-rolled walk would be a second implementation of
the loop, free to agree with itself while disagreeing with what ships.
### Mutations, each red for its stated reason
| mutation | result |
|---|---|
| the seat card loses its drop key — *the reported defect, reintroduced* | red, **and only the targeted test fired**; the general one stayed green |
| the table stops being a drop target | red — *"whose DROP id `table` is not an element in the document"* |
| the silent `return` comes back | red — *"a drop on nothing said `""`"* |
## 4. Silence was the second defect
`SCRIPT` did `if (!down || !up) { down = null; return; }` — no POST, no
status line, nothing at all. **That is why a human sitting in front of it
could not tell a broken target from a working page.**
The Rust side already held 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 right. Refusing
**silently** is not, and the two had been conflated.
The page now reports the raw fact — *"took action-attack, let go over
nothing droppable"*. It names elements, not moves, so ADR-0007 control 5
is intact and the body-shape assertion still holds.
## 5. What the human check cost, and what it bought
Stage 1 was held open on this check for **two passes** (CB-WP-0014,
CB-WP-0015), against a standing temptation to close it on green tests —
CB-EV-0012 §4 recorded that temptation explicitly and refused it.
It bought a defect that made the stage's headline interaction
**non-functional on its primary target**, plus a root cause in the document
model, plus a control class that did not exist. Two passes of delay was the
right price, and the reasoning that kept it open — *"no test in this repo
can reach it"* — was exactly correct rather than merely cautious.
**The stage does not close here either.** The fix is verified by tests, by
mutation, and against a live server; it is **not** verified by a human
dragging. That is the same standard that found this, and the same one that
would have missed it.
| pass | kind | responses | cost | $/response |
|---|---|---|---|---|
| **CB-WP-0015** | product | 136 | **$15.14** | 0.111 |
| CB-WP-0016 | product | *provisional — not quoted* | | |
CB-EV-0013 §5 found the self-quoting rule fixes the wrong boundary: a
pass's window runs to the *next* pass's first commit, so CB-WP-0015's
figure was still open when quoted. It is quoted here after CB-WP-0016's
declaration commit closed it, which is the first figure this project has
quoted at a boundary that had actually settled.
## 6. Open
- **INTENT stage 1: still one human verification**, now of a fix rather
than of an unknown. `cb-play --serve 0`, drag an action onto a seat
*card*.
- **The self-quoting rule still names the wrong boundary** — quote two
passes back. Owed since CB-EV-0013 §5.
- **AM-4b's scope defect (408,237 uncounted lines)** and its unmeasured
proc-macro share.
- **`python3` as a toolchain dependency was never argued.**
- **AM-4a cannot survive stage 2** — 1,741,979 against 161,000.
- **ADR-0007 D3's acquisition rule** remains unratified after deciding two
dependency questions.
- **Chaos: 11 of 12 declarations, 1 override.** The calibration window
closes on the next declaration and owes an evaluation.