Operator goal for 2026-09-26, a v13.2 engine-release blocker owned by @neo-fable: a tab torn out and dragged over another popup must show the target's drop zones, as a native popup drag does. @neo-fable's #19241 fixes where over the target the vessel converts. That region probe also saw preview: null 160 ms after the park settled at every entry sample, with previews only during continuous movement.
ADR 0029 §2.8.6 binds the missing half: "one successful platform settlement must be enough to materialize or retire the target proxy even when the hand stops moving". This ticket is that half.
The Problem
Measured headed on real windows (Chrome, no emulation), on dev@40c1262496. The run used @neo-fable's staging: main at 2632,100 (1320×920), target popup 700×500 at 4300,300, torn-out commits vessel 320×240. The pointer entered from the left void, then stopped:
entry (fx, fy)
+160 ms
+500 ms
+1 s
+3 s
one 1-px move, +160 ms
0.5, 0.3
converted, engaged, preview: null
null
null
null
tab-into, rendered
0.3, 0.3
converted, engaged, preview: null
null
null
null
tab-into, rendered
A still pointer never gets the drop zones.
A release there commits nothing, because DragTarget#onRemoteDrop commits currentPreview.
One pixel of motion is the entire difference.
The Architectural Reality
Park settles.TabSortZone#scheduleVesselConversionReplay re-enters DragCoordinator#onDragMove exactly once with the latest frame.
The target is cold when that frame arrives. While the park was transitioning, resolveRemoteDragTransition answered engage: false, so the coordinator never fed the target. Leaving the previous target ran onRemoteDragLeave → Participation#defaultClearPreview, which cleared the affordance geometry.
The first answer is null.DragTarget#onRemoteDragMove → Participation#defaultPreviewFor calls DragAffordances#ensureGeometry() without awaiting it. Its JSDoc documents that the first frames resolve to null while geometry settles. It then reads the synchronous geometry mirror, which is still cold.
Nothing asks again.getDomRect settles and geometry is set, but no later call to previewFor follows unless the pointer moves.
Why no spec caught it. The existing arm "settled initial park re-enters the coordinator without a second external pointer frame" (DockTabSortZone.spec.mjs) stubs the coordinator. It pins that one frame is replayed, never that the frame yields a preview.
The Fix
DragTarget owns its hover state (currentDragPayload / currentPreview), so it owns the one re-drive.
DragTarget.
Adds a seam, awaitPreviewable(payload): a thenable resolving true once the owner can answer a frame it could not yet answer; a non-thenable when nothing is pending.
Adds answerWhenPreviewable(payload). After a frame answers null, it re-runs previewFor(payload) once, only while that payload is still current: no newer frame, no leave, no terminal, not destroyed. No loop.
The error release is extracted to releasePreviewFailure(payload) for both paths.
Participation forwards the seam, with a default defaultAwaitPreviewable(): while the affordance mirror is cold, ensureGeometry() resolves to Boolean(geometry); otherwise null.
forwarded to the target; the default waits on the cold affordance geometry
host-supplied seam wins
member JSDoc
AC-1
DragTarget#onRemoteDragMove
ADR 0029 §2.3 target contract
unchanged synchronous answer, plus one fenced deferred answer after a null
—
method JSDoc
AC-1–AC-3
Acceptance Criteria
AC-1 (unit, red-first): one frame on a cold target renders the preview once geometry settles, with no second frame. The drop path holds an accepted preview, and the drop zones render.
AC-2 (unit): a newer frame wins, so the settled answer is for the latest pointer (edge-top, not tab-into).
AC-3 (unit): a leave, or a destroyed target, before geometry settles leaves no preview and makes no deferred previewFor call.
AC-4 (headed receipt in the PR): the stopped-pointer probe at the fix head shows preview and rendered at +160 ms, holding to +3 s, for both entries.
AC-5: the dashboard, draggable, manager and workstation unit populations stay green.
The ADR 0029 §2.8.6 park replay: DockTabSortZone.spec.mjs, "settled initial park re-enters the coordinator…".
Live latest-open sweep: checked the latest 20 open neo issues at 2026-09-26T08:01:46Z; no equivalent found. A2A: no claim on this symptom. Measurement requested by @neo-fable (MESSAGE:2b872727).
tobiu referenced in commit a9abb6c - "fix(dock): a still pointer gets the target's drop zones after the park (#19243) (#19244) on Sep 26, 2026, 10:48 AM
Context
Operator goal for 2026-09-26, a v13.2 engine-release blocker owned by @neo-fable: a tab torn out and dragged over another popup must show the target's drop zones, as a native popup drag does. @neo-fable's #19241 fixes where over the target the vessel converts. That region probe also saw
preview: null160 ms after the park settled at every entry sample, with previews only during continuous movement.ADR 0029 §2.8.6 binds the missing half: "one successful platform settlement must be enough to materialize or retire the target proxy even when the hand stops moving". This ticket is that half.
The Problem
Measured headed on real windows (Chrome, no emulation), on
dev@40c1262496. The run used @neo-fable's staging: main at 2632,100 (1320×920), target popup 700×500 at 4300,300, torn-outcommitsvessel 320×240. The pointer entered from the left void, then stopped:preview: nulltab-into, renderedpreview: nulltab-into, renderedDragTarget#onRemoteDropcommitscurrentPreview.The Architectural Reality
TabSortZone#scheduleVesselConversionReplayre-entersDragCoordinator#onDragMoveexactly once with the latest frame.transitioning,resolveRemoteDragTransitionansweredengage: false, so the coordinator never fed the target. Leaving the previous target ranonRemoteDragLeave→Participation#defaultClearPreview, which cleared the affordance geometry.null.DragTarget#onRemoteDragMove→Participation#defaultPreviewForcallsDragAffordances#ensureGeometry()without awaiting it. Its JSDoc documents that the first frames resolve tonullwhile geometry settles. It then reads the synchronousgeometrymirror, which is still cold.getDomRectsettles andgeometryis set, but no later call topreviewForfollows unless the pointer moves.DockTabSortZone.spec.mjs) stubs the coordinator. It pins that one frame is replayed, never that the frame yields a preview.The Fix
DragTargetowns its hover state (currentDragPayload/currentPreview), so it owns the one re-drive.awaitPreviewable(payload): a thenable resolvingtrueonce the owner can answer a frame it could not yet answer; a non-thenable when nothing is pending.answerWhenPreviewable(payload). After a frame answersnull, it re-runspreviewFor(payload)once, only while that payload is still current: no newer frame, no leave, no terminal, not destroyed. No loop.releasePreviewFailure(payload)for both paths.defaultAwaitPreviewable(): while the affordance mirror is cold,ensureGeometry()resolves toBoolean(geometry); otherwisenull.Contract Ledger
DragTargetconfigawaitPreviewable(payload) => Promise<Boolean> | non-thenableParticipationconfigawaitPreviewable+defaultAwaitPreviewableDragTarget#onRemoteDragMovenullAcceptance Criteria
acceptedpreview, and the drop zones render.edge-top, nottab-into).previewForcall.previewandrenderedat +160 ms, holding to +3 s, for both entries.Out of Scope
Avoided Traps
previewFor. It breaks the §2.3 synchronous answer the coordinator and the native path consume.Decision Record impact
aligned-with ADR 0029 §2.8.6. This implements the "even when the hand stops moving" sentence at the target tier; no amendment.
Related
DockTabSortZone.spec.mjs, "settled initial park re-enters the coordinator…".Live latest-open sweep: checked the latest 20 open neo issues at 2026-09-26T08:01:46Z; no equivalent found. A2A: no claim on this symptom. Measurement requested by @neo-fable (MESSAGE:2b872727).
Origin Session ID: 1b945fcf-1142-475f-8007-ac18d51c069a Retrieval Hint: "park replay one frame cold geometry mirror preview null stopped pointer re-drive"