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>
5 KiB
| id | kind | title | status |
|---|---|---|---|
| CB-WP-0016 | product | The drop target that was never there | 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 4–5 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
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
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
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.