Context
Leaf of #17578. That ticket spent five refuted hypotheses on DragCoordinator.resolveClaimedTarget, and a sixth (mine — an absent sort group) could not be settled by reading at all. The reason is not the difficulty of the bug: it is that the resolver's decisions were never observable.
The only evidence available was candidateDiagnostics, built by the workspace inside its own bail branch (if (!remoteSnapshot.ready)), iterating the sort group and calling zone.acceptsRemoteDrag() itself. That is a reconstruction of what the answer would have been, computed once readiness has already failed.
The Problem
The reconstruction cannot answer three questions that decide the search:
- Did the collection loop run at all?
resolveClaimedTarget returns at a !group guard above the loop. #17578 concluded the loop had run because pointerGestureToken was set — but that token is minted at DragCoordinator.mjs:688, one line above the resolver call at :689. Loop-never-iterated and token-present are consistent, so that branch was closed on a witness that could not carry it.
- Which conjunct failed? The gate is compound and
&& short-circuits: inner?.intersects({…}) && zone.acceptsRemoteDrag(screenX - inner.x, screenY - inner.y)
A nullish inner means acceptsRemoteDrag is never called. candidateDiagnostics records only that second conjunct, so "the zone refused" and "the zone was never asked" are the same observation.
- Was the source correctly skipped? The reconstruction reported
sameAsSource: false for both windows, including the source — it does not model the resolver's own skip rule.
The cost is measured, not hypothetical: six hypotheses, at least two of them formed and published against a reconstruction that structurally could not refute them.
The Fix
resolveClaimedTarget records what it actually did, on a bounded ring surfaced through the route that already reaches a spec (toJSON → RuntimeService.getDragState):
- each conjunct separately —
innerResolved, intersects, accepts — so a refusal is distinguishable from a zone never asked;
- skipped candidates with their reason (
source-or-excluded, no-stable-identity);
groupSize and a distinct group-absent outcome, so the early return is visible rather than indistinguishable from an ordinary no-claim;
- bounded to 40 entries — one gesture emits one per pointer move, so the ring covers the tail of a single drag and cannot accumulate across a session.
Not a console.log. Neo.manager.DragCoordinator runs in the App Worker and neither fixtures.mjs nor the spec forwards browser console to Playwright's stdout, so a probe there is structurally invisible — #17578 documents this, and I re-derived the same negative before re-reading it.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback / edge case |
Docs |
Evidence |
DragCoordinator.claimTrace |
the resolver's own decisions |
bounded ring of per-resolution observations, newest last |
ring never exceeds claimTraceLimit; a gesture cannot grow it without bound |
member JSDoc |
unit arm over claimTraceLimit + 25 moves |
| Per-candidate record |
the compound gate's two conjuncts |
innerResolved / intersects / accepts recorded independently |
a zone never asked reports accepts: null, never false |
method JSDoc |
unit arm where the zone refuses and all three are asserted |
| Early return |
!group guard above the loop |
its own outcome: 'group-absent' with groupSize: null |
never collapses into no-claim |
inline comment at the guard |
unit arm asserting the distinct outcome |
| Debug route |
toJSON → getDragState |
the trace rides the existing drag-state payload |
no new transport, no new tool |
— |
the cross-window spec prints it on failure |
Acceptance Criteria
Out of Scope
- Fixing the cross-window drag defect itself — that stays on #17578, whose five ACs all describe the fix and which this leaf deliberately does not close.
- The vessel-conversion admission seam (
dockVesselConversionIn), which the trace has now narrowed to but which is a separate measurement.
- Any new transport, MCP tool, or Neural Link surface.
Context
Leaf of #17578. That ticket spent five refuted hypotheses on
DragCoordinator.resolveClaimedTarget, and a sixth (mine — an absent sort group) could not be settled by reading at all. The reason is not the difficulty of the bug: it is that the resolver's decisions were never observable.The only evidence available was
candidateDiagnostics, built by the workspace inside its own bail branch (if (!remoteSnapshot.ready)), iterating the sort group and callingzone.acceptsRemoteDrag()itself. That is a reconstruction of what the answer would have been, computed once readiness has already failed.The Problem
The reconstruction cannot answer three questions that decide the search:
resolveClaimedTargetreturns at a!groupguard above the loop. #17578 concluded the loop had run becausepointerGestureTokenwas set — but that token is minted atDragCoordinator.mjs:688, one line above the resolver call at:689. Loop-never-iterated and token-present are consistent, so that branch was closed on a witness that could not carry it.&&short-circuits:inner?.intersects({…}) && zone.acceptsRemoteDrag(screenX - inner.x, screenY - inner.y)A nullishinnermeansacceptsRemoteDragis never called.candidateDiagnosticsrecords only that second conjunct, so "the zone refused" and "the zone was never asked" are the same observation.sameAsSource: falsefor both windows, including the source — it does not model the resolver's own skip rule.The cost is measured, not hypothetical: six hypotheses, at least two of them formed and published against a reconstruction that structurally could not refute them.
The Fix
resolveClaimedTargetrecords what it actually did, on a bounded ring surfaced through the route that already reaches a spec (toJSON→RuntimeService.getDragState):innerResolved,intersects,accepts— so a refusal is distinguishable from a zone never asked;source-or-excluded,no-stable-identity);groupSizeand a distinctgroup-absentoutcome, so the early return is visible rather than indistinguishable from an ordinary no-claim;Not a
console.log.Neo.manager.DragCoordinatorruns in the App Worker and neitherfixtures.mjsnor the spec forwards browser console to Playwright's stdout, so a probe there is structurally invisible — #17578 documents this, and I re-derived the same negative before re-reading it.Contract Ledger
DragCoordinator.claimTraceclaimTraceLimit; a gesture cannot grow it without boundclaimTraceLimit + 25movesinnerResolved/intersects/acceptsrecorded independentlyaccepts: null, neverfalse!groupguard above the loopoutcome: 'group-absent'withgroupSize: nullno-claimtoJSON→getDragStateAcceptance Criteria
groupSize: null, and never as an ordinary no-claim.claimTraceLimitleaves exactlyclaimTraceLimitentries.group-absentintono-claimturns exactly one arm red, so the distinction is load-bearing rather than decorative.Out of Scope
dockVesselConversionIn), which the trace has now narrowed to but which is a separate measurement.