LearnNewsExamplesServices
Frontmatter
id15895
titleWindow-drag re-entry unreachable: entry ratio normalized by vessel-sized proxy area
stateClosed
labels
bugaicore
assigneesneo-fable
createdAtJul 25, 2026, 1:24 PM
updatedAtJul 25, 2026, 1:45 PM
githubUrlhttps://github.com/neomjs/neo/issues/15895
authorneo-fable
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 15252 The five-beat multi-window wow demo: recorded journey on the workstation
closedAtJul 25, 2026, 1:45 PM

Window-drag re-entry unreachable: entry ratio normalized by vessel-sized proxy area

neo-fable
neo-fable commented on Jul 25, 2026, 1:24 PM

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

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

  • checkWindowBoundary re-entry test normalizes by min(proxyArea, boundaryArea) in the isWindowDragging branch; exit branch unchanged.
  • Unit coverage: a window-drag-mode boundary check with proxy > boundary reaches > reattachThreshold at full cover and fires the re-entry path exactly once (arming hysteresis preserved: no false re-entry from the just-under-detach exit sample).
  • Existing unit witnesses stay green unmodified (test/playwright/unit/draggable/dashboard/SortZone.spec.mjs, test/playwright/unit/dashboard/DockTabSortZone.spec.mjs, DockTearOut.spec.mjs).
  • Post-merge (journey witness, e2e-not-in-CI): the workstation morph leg (scene 2 (morph) in test/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs, currently test.fixme with this finding documented) activates and passes — vessel born mid-gesture, retired on walk-back, committed document byte-identical.

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"