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

6.6 KiB

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 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 — 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.