Context
The dock line shipped a full model layer — transferItem, moveItem, moveNode, splitNode, resizeSplit and friends in DockZoneModel (#14768 and siblings), all with green unit specs. But green unit specs prove the MODEL math; they say nothing about whether a rendered dock does anything on a real gesture. The FIRST whitebox-e2e driven against the rendered example (examples/dashboard/dock/) proved the gap loud and clear: main-tabs order ['strategy','swarm'] UNCHANGED after a native tab-header drag, draggableCount: 2 — only the two splitters were draggable; the tab headers were not draggable at all. Every op was JSON math never wired to a gesture. This is the "I have not seen ONE visual example of the dock engine" finding, made falsifiable.
The Problem
DockLayoutAdapter.projectTabsNode projected each tabs node into a Neo.tab.Container config, but left dragResortable at its default (false). Result: the projected tab headers rendered static, no drag sensor armed, and no gesture ever reached the committed dockZone.v1 document. The "missing middle" — drag-source → drop → applyDockZoneOperation → committed model — was unbuilt for the simplest gesture (reorder a tab within its own zone).
The Architectural Reality
- Owner tier:
src/dashboard/DockLayoutAdapter.mjs#projectTabsNode (the projection SSOT the example re-projects from).
- Reuse, not rebuild (ADR 0029 "no parallel drag system"):
Neo.tab.Container already owns tab-header drag-resort via dragResortable → the existing Neo.draggable.tab.header.toolbar.SortZone, firing a moveTo event {fromIndex, toIndex} on drop (src/tab/Container.mjs:519). No new drag machinery.
- Commit seam: the container's
moveTo listener maps items[fromIndex] → context.applyDockZoneOperation({operation:'addTab', itemId, tabsNodeId, index:toIndex}). DockZoneModel's addTab handler self-routes an already-present item to moveItem (DockZoneModel.mjs:84), and addTab detaches-before-insert (:1672), so a within-node reorder never duplicates. The committed document is re-projected through context.onDockZoneDocumentChange — the same commit loop DockSplitter.commitResizeSplit uses. That callback's one-tick timeout(0) deferral (MainContainer.mjs:288) protects the drag-end handler from the workspace rebuild's use-after-destroy.
The Fix
One PR. projectTabsNode gains dragResortable: true + a moveTo listener that commits the reorder through the landed operation seam (applyDockZoneOperation + onDockZoneDocumentChange). The whitebox-e2e test/playwright/e2e/DockDragDropNL.spec.mjs ships in the same PR (per the epic's gesture-proof guardrail): it drives a native page.mouse drag of the "Strategy" header past "Swarm" and asserts App-Worker truth — main-tabs goes ['strategy','swarm'] → ['swarm','strategy'] — plus that the tab headers are actually draggable in the DOM. No framework change: the reference example (examples/tab/container/, dragResortable:true) already proves the primitive; the dock only had to project it.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
DockLayoutAdapter.projectTabsNode tab-container config |
ADR 0029 §"no parallel drag system"; src/tab/Container.mjs dragResortable (:75) |
Emits dragResortable:true so the projected tab headers ride the existing header SortZone |
Railed (auto-hidden) items are pre-filtered from items; fromIndex/toIndex map to the visible set |
JSDoc on projectTabsNode names the reuse + the cross-zone follow-up |
e2e DockDragDropNL: draggableCount ≥ 6, model reorders |
moveTo listener → context.applyDockZoneOperation |
Neo.tab.Container#moveTo event {fromIndex,toIndex} (:536); DockZoneModel.addTab handler (:84) |
Within-node reorder commits via addTab→moveItem self-route; detach-before-insert = no duplication |
On errors[] the document is left untouched (fail-closed {document,errors}) |
inline comment in projectTabsNode |
unit specs 123 green; e2e worker-truth before/after |
Decision Record impact
aligned-with ADR 0029 — this leaf is the empirical validation of the "no parallel drag system" mandate: the dock's first interactive gesture rides the existing tab.Container/SortZone pipeline, not a new one.
Acceptance Criteria
Out of Scope
- Cross-window drag / foreign-item transfer (
#14769) and its two-window demo (#14772).
- Grouped drag (
#14770), tab overflow affordance (#14771), transition animations (#14779).
- Any
src/tab/ framework change — the primitive already works; this leaf is projection-only.
Related
Parent #13158 · authority ADR 0029 §2.6 / §"no parallel drag system" · foundation the cross-window wiring #14769 builds on · sibling e2e-coverage #14591.
Live latest-open sweep: checked latest 40 open enhancement issues on 2026-07-05; no equivalent within-container tab-drag gesture ticket found (#14769 is cross-window, #14771 overflow, #14772 demo). A2A in-flight sweep: mailbox scanned same session, no competing lane-claim.
Origin Session ID: 9e42a8de-4291-46fc-944e-92ceb0db1748
Retrieval Hint: "dock tab drag dragResortable moveTo commit within-container reorder whitebox e2e"
Context
The dock line shipped a full model layer —
transferItem,moveItem,moveNode,splitNode,resizeSplitand friends inDockZoneModel(#14768 and siblings), all with green unit specs. But green unit specs prove the MODEL math; they say nothing about whether a rendered dock does anything on a real gesture. The FIRST whitebox-e2e driven against the rendered example (examples/dashboard/dock/) proved the gap loud and clear:main-tabsorder['strategy','swarm']UNCHANGED after a native tab-header drag,draggableCount: 2— only the two splitters were draggable; the tab headers were not draggable at all. Every op was JSON math never wired to a gesture. This is the "I have not seen ONE visual example of the dock engine" finding, made falsifiable.The Problem
DockLayoutAdapter.projectTabsNodeprojected each tabs node into aNeo.tab.Containerconfig, but leftdragResortableat its default (false). Result: the projected tab headers rendered static, no drag sensor armed, and no gesture ever reached the committeddockZone.v1document. The "missing middle" — drag-source → drop →applyDockZoneOperation→ committed model — was unbuilt for the simplest gesture (reorder a tab within its own zone).The Architectural Reality
src/dashboard/DockLayoutAdapter.mjs#projectTabsNode(the projection SSOT the example re-projects from).Neo.tab.Containeralready owns tab-header drag-resort viadragResortable→ the existingNeo.draggable.tab.header.toolbar.SortZone, firing amoveToevent{fromIndex, toIndex}on drop (src/tab/Container.mjs:519). No new drag machinery.moveTolistener mapsitems[fromIndex]→context.applyDockZoneOperation({operation:'addTab', itemId, tabsNodeId, index:toIndex}).DockZoneModel'saddTabhandler self-routes an already-present item tomoveItem(DockZoneModel.mjs:84), andaddTabdetaches-before-insert (:1672), so a within-node reorder never duplicates. The committed document is re-projected throughcontext.onDockZoneDocumentChange— the same commit loopDockSplitter.commitResizeSplituses. That callback's one-ticktimeout(0)deferral (MainContainer.mjs:288) protects the drag-end handler from the workspace rebuild's use-after-destroy.The Fix
One PR.
projectTabsNodegainsdragResortable: true+ amoveTolistener that commits the reorder through the landed operation seam (applyDockZoneOperation+onDockZoneDocumentChange). The whitebox-e2etest/playwright/e2e/DockDragDropNL.spec.mjsships in the same PR (per the epic's gesture-proof guardrail): it drives a nativepage.mousedrag of the "Strategy" header past "Swarm" and asserts App-Worker truth —main-tabsgoes['strategy','swarm']→['swarm','strategy']— plus that the tab headers are actually draggable in the DOM. No framework change: the reference example (examples/tab/container/,dragResortable:true) already proves the primitive; the dock only had to project it.Contract Ledger Matrix
DockLayoutAdapter.projectTabsNodetab-container configsrc/tab/Container.mjsdragResortable(:75)dragResortable:trueso the projected tab headers ride the existing header SortZoneitems;fromIndex/toIndexmap to the visible setprojectTabsNodenames the reuse + the cross-zone follow-upDockDragDropNL:draggableCount ≥ 6, model reordersmoveTolistener →context.applyDockZoneOperationNeo.tab.Container#moveToevent{fromIndex,toIndex}(:536);DockZoneModel.addTabhandler (:84)addTab→moveItemself-route; detach-before-insert = no duplicationerrors[]the document is left untouched (fail-closed{document,errors})projectTabsNodeDecision Record impact
aligned-with ADR 0029— this leaf is the empirical validation of the "no parallel drag system" mandate: the dock's first interactive gesture rides the existingtab.Container/SortZone pipeline, not a new one.Acceptance Criteria
dragResortableon projected tabs nodes) — DOM-verifiable in the e2e.dockZone.v1document (App-Worker truth), fail-closed onerrors[].test/playwright/e2e/DockDragDropNL.spec.mjsdrives the native gesture and asserts the before/after model change (ships in this PR).DockLayoutAdapter+DockZoneModelunit specs stay green (no projection-shape regression).Out of Scope
#14769) and its two-window demo (#14772).#14770), tab overflow affordance (#14771), transition animations (#14779).src/tab/framework change — the primitive already works; this leaf is projection-only.Related
Parent
#13158· authority ADR 0029 §2.6 / §"no parallel drag system" · foundation the cross-window wiring#14769builds on · sibling e2e-coverage#14591.Live latest-open sweep: checked latest 40 open
enhancementissues on 2026-07-05; no equivalent within-container tab-drag gesture ticket found (#14769is cross-window,#14771overflow,#14772demo). A2A in-flight sweep: mailbox scanned same session, no competing lane-claim.Origin Session ID: 9e42a8de-4291-46fc-944e-92ceb0db1748 Retrieval Hint: "dock tab drag dragResortable moveTo commit within-container reorder whitebox e2e"