Context
Direct successor to #15895, found by exactly the post-merge validation smoke that PR #15897 named (and that PR #15897's reviewer recorded as her challenge's falsifier): with min-area coverage the re-entry RATIO now crosses the threshold on a real pointer walk-back (measured 0.7898 > 0.6, armed, rising) — and the vessel still never retires. Three-hop event probes on the live gesture bisected it: dragBoundaryEntry never fires on the zone (boundaryEntrySeen=false) even though lastIntersectionRatio updates on the crossing sample — the entry branch RAN and died between the condition and the fire.
The Problem
The re-entry block in Neo.draggable.container.SortZone#checkWindowBoundary restores the in-window layout BEFORE firing the event, and that restore is written for wrapperStyle-managed zones:
me.dragPlaceholder.wrapperStyle = {...} — unconditional dereference;
- the
itemRects.forEach loop assigns item.wrapperStyle where item = me.owner.items[mappedIndex] is only guarded against placeholder/dragComponent identity, not against undefined.
Dock tab strips are placeholder-less by design — Neo.dashboard.DockTabSortZone's own docs state a tab strip's geometry is owned by the committed model projection ("No dashboard layout re-flow"). Live probe on the workstation stage: placeholder=false at the crossing sample. The first restore statement throws TypeError, the exception dies inside the DOM-event dispatch (nothing reaches pageerror), fire('dragBoundaryEntry') is never reached, endWindowDrag never runs, and the gesture is stuck in window-drag with the entry condition true on every subsequent sample — throwing every time.
The existing unit fixture (test/playwright/unit/draggable/dashboard/SortZone.spec.mjs) mocks dragPlaceholder, which is exactly why the class-level tests never see this.
The Architectural Reality
- The entry block conflates two concerns: (a) the universal gesture fact ("the drag re-entered — fire the event, the host decides"), and (b) the dashboard-family layout restore (placeholder visibility + wrapperStyle re-application). Only (b) is zone-family-specific.
- For projection-owned zones the restore is semantically a NO-OP, not a missing feature: there is no captured in-window layout to restore, the projection owns geometry. Guarding is contract-encoding, not defensive masking (the ADR-0019 B-family concern does not apply: nothing is hidden — the zone class genuinely has no placeholder by design).
- The exit path has no such coupling (it fires before any restore), which is why tear-out exit works everywhere while entry only ever worked on dashboard-style zones.
The Fix
In the re-entry block:
- Gate the layout-restore steps (placeholder visibility + the
itemRects wrapperStyle loop) on me.dragPlaceholder — the marker of a wrapperStyle-managed zone.
- Inside the managed-zone loop, skip
undefined items (mappedIndex can point past live items during transitional states).
fire('dragBoundaryEntry', ...) stays unconditional — the event is the universal part.
One file (src/draggable/container/SortZone.mjs), plus a unit regression: a placeholder-less window-drag zone whose full-cover walk-in must FIRE dragBoundaryEntry without throwing (RED today: the throw swallows the fire).
Acceptance Criteria
Out of Scope
- Extracting the restore into an overridable zone-family method (worth considering if a third zone family appears; two call sites do not justify the seam yet).
- The
#15895 normalization semantics (shipped, correct, and confirmed working by this very investigation's ratio receipts).
Related
#15895 / PR #15897 (sibling defect, same entry path; this one was masked BEHIND it — unreachable ratio meant the throwing block never executed before today)
#15252 / PR #15840 (the workstation morph witness is the activation receipt; probe logs fivebeat-morph-probe2/3 in the production root)
Live latest-open sweep: checked latest 12 open issues at 2026-07-25T11:57Z; no equivalent. A2A herd-window sweep (10 msgs, all read-states): no overlapping claim.
Origin Session ID: b8c9e338-27b3-4385-99ac-dce459733940
Retrieval Hint: "placeholder-less sort zone dragBoundaryEntry throw morph walk-back"
Context
Direct successor to
#15895, found by exactly the post-merge validation smoke that PR #15897 named (and that PR #15897's reviewer recorded as her challenge's falsifier): with min-area coverage the re-entry RATIO now crosses the threshold on a real pointer walk-back (measured0.7898 > 0.6, armed, rising) — and the vessel still never retires. Three-hop event probes on the live gesture bisected it:dragBoundaryEntrynever fires on the zone (boundaryEntrySeen=false) even thoughlastIntersectionRatioupdates on the crossing sample — the entry branch RAN and died between the condition and the fire.The Problem
The re-entry block in
Neo.draggable.container.SortZone#checkWindowBoundaryrestores the in-window layout BEFORE firing the event, and that restore is written for wrapperStyle-managed zones:me.dragPlaceholder.wrapperStyle = {...}— unconditional dereference;itemRects.forEachloop assignsitem.wrapperStylewhereitem = me.owner.items[mappedIndex]is only guarded against placeholder/dragComponent identity, not againstundefined.Dock tab strips are placeholder-less by design —
Neo.dashboard.DockTabSortZone's own docs state a tab strip's geometry is owned by the committed model projection ("No dashboard layout re-flow"). Live probe on the workstation stage:placeholder=falseat the crossing sample. The first restore statement throwsTypeError, the exception dies inside the DOM-event dispatch (nothing reaches pageerror),fire('dragBoundaryEntry')is never reached,endWindowDragnever runs, and the gesture is stuck in window-drag with the entry condition true on every subsequent sample — throwing every time.The existing unit fixture (
test/playwright/unit/draggable/dashboard/SortZone.spec.mjs) mocksdragPlaceholder, which is exactly why the class-level tests never see this.The Architectural Reality
The Fix
In the re-entry block:
itemRectswrapperStyle loop) onme.dragPlaceholder— the marker of a wrapperStyle-managed zone.undefineditems (mappedIndexcan point past live items during transitional states).fire('dragBoundaryEntry', ...)stays unconditional — the event is the universal part.One file (
src/draggable/container/SortZone.mjs), plus a unit regression: a placeholder-less window-drag zone whose full-cover walk-in must FIREdragBoundaryEntrywithout throwing (RED today: the throw swallows the fire).Acceptance Criteria
dragBoundaryEntryexactly once on the crossing sample (unit witness, RED→GREEN).scene 2 (morph),test/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs) passes end-to-end: vessel born mid-gesture, retired on walk-back viadockTearOutEntry, committed document byte-identical.Out of Scope
#15895normalization semantics (shipped, correct, and confirmed working by this very investigation's ratio receipts).Related
#15895/ PR #15897 (sibling defect, same entry path; this one was masked BEHIND it — unreachable ratio meant the throwing block never executed before today)#15252/ PR #15840 (the workstation morph witness is the activation receipt; probe logsfivebeat-morph-probe2/3in the production root)Live latest-open sweep: checked latest 12 open issues at 2026-07-25T11:57Z; no equivalent. A2A herd-window sweep (10 msgs, all read-states): no overlapping claim.
Origin Session ID: b8c9e338-27b3-4385-99ac-dce459733940
Retrieval Hint: "placeholder-less sort zone dragBoundaryEntry throw morph walk-back"