Context
Found while wiring the workstation five-beat capstone (#15252, PR #15840): the film's signature beat — one continuous drag OUT past the window edge (vessel born mid-gesture) and BACK IN (vessel retires, the in-window proxy resumes) — cannot fire from a real pointer. The operator's steer to exercise popup movement and hover states predicted exactly this class of finding. The workstation's executeTearOutStep({reenter: true}) witness surfaced it with full geometry receipts.
The Problem
Neo.draggable.container.SortZone#checkWindowBoundary (src/draggable/container/SortZone.mjs:282-330) computes intersectionRatio = intersectionArea / proxyArea and requires intersectionRatio > reattachThreshold (default 0.6) for the re-entry that fires dragBoundaryEnter → dockTearOutEntry.
During window-drag, the DragDrop main addon mints the move payload's proxy rect at vessel size (src/main/addon/DragDrop.mjs:391: new DOMRect(x - window.screenX, y - window.screenY, me.popupWidth, me.popupHeight)), while the boundary rect stays the tab strip (no boundaryContainerId override exists anywhere in the dock stack — verified by grep across src/dashboard/DockLayoutAdapter.mjs, src/dashboard/DockTabSortZone.mjs, src/tab/header/Toolbar.mjs).
The achievable maximum ratio is therefore boundaryArea / proxyArea. Measured live (workstation stage, viewport 1280×800):
- boundary (strip of
right-top-tabs): {x: 954, y: 110, width: 314, height: 49} → area 15386
- window-drag proxy:
320 × 240 (the vessel dims openTearOutVessel minted) → area 76800
- theoretical max ratio:
15386 / 76800 = 0.20033854…
- observed at full strip cover (pointer walked back to the source button):
lastRatio = 0.20033854166666668 — the exact theoretical maximum
- observed at a boundary-interior point:
0.158
0.2003 < 0.6 for every pointer position: dockTearOutEntry can never fire on this geometry. The gesture's own contract says re-entry is first-class (DockTabSortZone docs: "The drag re-entered the source window past the reattach threshold"; the arming hysteresis at SortZone:298-323 exists solely to serve it), and DockTearOut.mjs's onDockTearOutEntry implements the zero-mutation vessel retirement — but the only witnesses are unit-level (test/playwright/unit/dashboard/DockTearOut.spec.mjs, DockTabSortZone.spec.mjs — mocked data, no geometry). No journey-level witness ever drove a real re-entry; the reachable-geometry gap was invisible.
The Architectural Reality
- The in-window sort regime is self-consistent: pre-window-drag,
proxyRect is the DOM proxy element's rect (DragDrop.mjs:354/:444) — item-scale vs its strip, ratio ≈ 1 inside, drops on exit. Exit arming and detachThreshold semantics are healthy (the workstation tear-out COMMIT leg passes green with them).
- The size mismatch exists only in the
isWindowDragging branch: the proxy embodies the future vessel (popupWidth × popupHeight) while the boundary stays strip-scale. Normalizing by the proxy area makes the ratio's ceiling collapse exactly when re-entry is the question being asked.
reattachThreshold itself is fine for same-scale regimes; retuning it per-app would be a hack around the wrong denominator (a threshold below 0.2 here would still break on the next geometry).
The Fix
Scope the normalization to the smaller rect in the window-drag re-entry test only: in checkWindowBoundary's isWindowDragging branch, compute the coverage ratio as intersectionArea / min(proxyArea, boundaryArea).
- Classic regime (proxy ≤ boundary):
min = proxy area → byte-identical behavior to today.
- Window-drag vessel regime (proxy > boundary):
min = boundary area → full strip cover = 1.0, re-entry threshold semantics become "the returning proxy substantially covers the strip" — the natural re-dock gesture.
- The exit branch (
!isWindowDragging, :298-311) stays untouched: zero behavior change for every existing gesture; the only reachable-behavior delta is in the regime that is provably dead today.
One file (src/draggable/container/SortZone.mjs), a few lines, plus unit coverage for the window-drag normalization.
Acceptance Criteria
Out of Scope
- Retuning
reattachThreshold / detachThreshold defaults.
- Redefining the boundary rect to the window (a different, larger design conversation; the strip-coverage semantics are the natural re-dock gesture).
- The workstation film/capture work itself (
#15252 / PR #15840 carries it).
Avoided Traps
- Per-zone threshold threading via the adapter (rejected): treats the symptom; every future geometry pair needs re-tuning, and a sub-
0.2 threshold would make the exit-side arming window degenerate.
- Boundary swap to window rect during window-drag (rejected for this ticket): changes the user-facing meaning of re-entry (window-hover vs strip-hover) and touches the exit grammar's arming assumptions; candidate for a design discussion if strip-coverage proves too eager in practice.
Related
#15252 (five-beat capstone — the film's cold-open morph beat consumes this; the ready witness executeTearOutStep({reenter: true}) ships in PR #15840)
- PR #15840 (receipts:
fivebeat-morph-run3..5 logs in the production root; the fixme'd morph leg names this ticket's mechanism)
- Docking design record §2.8 multi-window amendment (re-entry as first-class gesture outcome) — Decision Record impact:
aligned-with (restores a documented contract; no ADR challenged)
Live latest-open sweep: checked latest 20 open issues at 2026-07-25T11:23Z; no equivalent found. A2A herd-window sweep (30 msgs, all read-states): no overlapping claim.
Origin Session ID: b8c9e338-27b3-4385-99ac-dce459733940
Retrieval Hint: "window-drag re-entry reattachThreshold proxy-area normalization morph beat"
Context
Found while wiring the workstation five-beat capstone (
#15252, PR #15840): the film's signature beat — one continuous drag OUT past the window edge (vessel born mid-gesture) and BACK IN (vessel retires, the in-window proxy resumes) — cannot fire from a real pointer. The operator's steer to exercise popup movement and hover states predicted exactly this class of finding. The workstation'sexecuteTearOutStep({reenter: true})witness surfaced it with full geometry receipts.The Problem
Neo.draggable.container.SortZone#checkWindowBoundary(src/draggable/container/SortZone.mjs:282-330) computesintersectionRatio = intersectionArea / proxyAreaand requiresintersectionRatio > reattachThreshold(default0.6) for the re-entry that firesdragBoundaryEnter→dockTearOutEntry.During window-drag, the DragDrop main addon mints the move payload's proxy rect at vessel size (src/main/addon/DragDrop.mjs:391:
new DOMRect(x - window.screenX, y - window.screenY, me.popupWidth, me.popupHeight)), while the boundary rect stays the tab strip (noboundaryContainerIdoverride exists anywhere in the dock stack — verified by grep acrosssrc/dashboard/DockLayoutAdapter.mjs,src/dashboard/DockTabSortZone.mjs,src/tab/header/Toolbar.mjs).The achievable maximum ratio is therefore
boundaryArea / proxyArea. Measured live (workstation stage, viewport 1280×800):right-top-tabs):{x: 954, y: 110, width: 314, height: 49}→ area 15386320 × 240(the vessel dimsopenTearOutVesselminted) → area 7680015386 / 76800 = 0.20033854…lastRatio = 0.20033854166666668— the exact theoretical maximum0.1580.2003 < 0.6for every pointer position:dockTearOutEntrycan never fire on this geometry. The gesture's own contract says re-entry is first-class (DockTabSortZonedocs: "The drag re-entered the source window past the reattach threshold"; the arming hysteresis at SortZone:298-323 exists solely to serve it), andDockTearOut.mjs'sonDockTearOutEntryimplements the zero-mutation vessel retirement — but the only witnesses are unit-level (test/playwright/unit/dashboard/DockTearOut.spec.mjs,DockTabSortZone.spec.mjs— mocked data, no geometry). No journey-level witness ever drove a real re-entry; the reachable-geometry gap was invisible.The Architectural Reality
proxyRectis the DOM proxy element's rect (DragDrop.mjs:354/:444) — item-scale vs its strip, ratio ≈ 1 inside, drops on exit. Exit arming anddetachThresholdsemantics are healthy (the workstation tear-out COMMIT leg passes green with them).isWindowDraggingbranch: the proxy embodies the future vessel (popupWidth × popupHeight) while the boundary stays strip-scale. Normalizing by the proxy area makes the ratio's ceiling collapse exactly when re-entry is the question being asked.reattachThresholditself is fine for same-scale regimes; retuning it per-app would be a hack around the wrong denominator (a threshold below0.2here would still break on the next geometry).The Fix
Scope the normalization to the smaller rect in the window-drag re-entry test only: in
checkWindowBoundary'sisWindowDraggingbranch, compute the coverage ratio asintersectionArea / min(proxyArea, boundaryArea).min= proxy area → byte-identical behavior to today.min= boundary area → full strip cover = 1.0, re-entry threshold semantics become "the returning proxy substantially covers the strip" — the natural re-dock gesture.!isWindowDragging, :298-311) stays untouched: zero behavior change for every existing gesture; the only reachable-behavior delta is in the regime that is provably dead today.One file (
src/draggable/container/SortZone.mjs), a few lines, plus unit coverage for the window-drag normalization.Acceptance Criteria
checkWindowBoundaryre-entry test normalizes bymin(proxyArea, boundaryArea)in theisWindowDraggingbranch; exit branch unchanged.> reattachThresholdat full cover and fires the re-entry path exactly once (arming hysteresis preserved: no false re-entry from the just-under-detach exit sample).test/playwright/unit/draggable/dashboard/SortZone.spec.mjs,test/playwright/unit/dashboard/DockTabSortZone.spec.mjs,DockTearOut.spec.mjs).scene 2 (morph)intest/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs, currentlytest.fixmewith this finding documented) activates and passes — vessel born mid-gesture, retired on walk-back, committed document byte-identical.Out of Scope
reattachThreshold/detachThresholddefaults.#15252/ PR #15840 carries it).Avoided Traps
0.2threshold would make the exit-side arming window degenerate.Related
#15252(five-beat capstone — the film's cold-open morph beat consumes this; the ready witnessexecuteTearOutStep({reenter: true})ships in PR #15840)fivebeat-morph-run3..5logs in the production root; the fixme'd morph leg names this ticket's mechanism)aligned-with(restores a documented contract; no ADR challenged)Live latest-open sweep: checked latest 20 open issues at 2026-07-25T11:23Z; no equivalent found. A2A herd-window sweep (30 msgs, all read-states): no overlapping claim.
Origin Session ID: b8c9e338-27b3-4385-99ac-dce459733940
Retrieval Hint: "window-drag re-entry reattachThreshold proxy-area normalization morph beat"