clay-borg/workplans/CB-WP-0016-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

7.3 KiB
Raw Blame History

id kind title status
CB-WP-0016 product The drop target that was never there done

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

id: CB-WP-0016-T01
status: done
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.

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

id: CB-WP-0016-T02
status: done
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.

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

id: CB-WP-0016-T03
status: done
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.

Done 2026-08-02. CB-EV-0014. 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.