clay-borg/workplans/CB-WP-0016-the-drop-target.md
tegwick b87dbec55b
Some checks failed
ci / check (push) Failing after 4s
Sync hub IDs and work-record index for CB-WP-0016
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:49:21 +02:00

185 lines
7.6 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.

---
id: CB-WP-0016
kind: product
title: "The drop target that was never there"
status: done
state_hub_workstream_id: "87c1563d-5969-4355-8e25-c27a07efc0a7"
---
# 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: done
priority: high
state_hub_task_id: "70e50904-a8a7-4bcc-b02b-6e5cd7eea9f2"
```
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.
**Done 2026-08-02.** The aliasing was not needed, because the constraint
that caused the defect was removed instead.
**Drop keys are now `data-drop`, not `id`.** An `id` must be unique, so
exactly one element could ever be `seat-0` — the graph circle took it and
the card went without. Any number of elements may carry the same
`data-drop`, so a seat is droppable on both of its drawings. Measured on
the live page: `seat-0/1/2` each appear **twice**, and `id` survives on
exactly one element, `cb-status`, which is the only one the script looks
up. `down=action-attack&up=seat-1` against a real server returns `ok`.
The mutation that reintroduces the reported defect — the seat card losing
its key — goes red, **and only the targeted test fires**; the general
existence check stays green. That is the point of T03's finding.
## Task: a gesture that lands nowhere must say so
```task
id: CB-WP-0016-T02
status: done
priority: high
state_hub_task_id: "2602ec51-36e8-4253-b785-8d4fa5431aa2"
```
`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.
**Done 2026-08-02.** The page now writes *"took action-attack, let go over
nothing droppable"* to the status line. It names elements, not moves, so
control 5 holds and the body-shape assertion is untouched. Restoring the
bare `return` turns
`jsrun::tests::a_drop_on_nothing_reports_instead_of_going_quiet` red with
*"a drop on nothing said `\"\"`"*.
The JS DOM stub had to grow a real `getAttribute` and a null-parent node
to model a pointer landing on nothing — it previously could not express
the case at all, which is part of why the silence was invisible.
## Task: the check that closes the class
```task
id: CB-WP-0016-T03
status: done
priority: high
state_hub_task_id: "01ec9542-08b2-421f-9e59-968eb534dab5"
```
**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.
**Done 2026-08-02.**
[CB-EV-0014](../evidence/CB-EV-0014-the-drop-target.md). `make all`
exits 0.
- **The general check does not catch the reported defect, and the evidence
says so plainly.** `seat-0` existed — on the graph circle — so an
existence check over the whole document cannot tell that the element the
user is *pointed at* is not the one that works. 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.
- **Stage 1 does not close here either.** The fix is verified by tests, by
mutation, and against a live server — not by a human dragging, which is
the standard that found it.
- **A cb-play assertion drifted**: it matched `id=\"action-ground\"` as a
substring and silently described itself as checking the page. Rewritten
through `doc::drop_keys`.