Context and problem
Reported by @tobiu on merged dev: dock layouts offer a 5-arm cross inside a container and 4 chips at the root edges. The root top/bottom dropzones span the full width and the sides the full height — but dropping at the bottom in apps/workstation does not produce what the preview drew. The side zones win and the dropped zone comes out narrower.
His framing is the right one: "a classic border-layouts problem — which corners get more weight."
The mechanism, located
The semantic model is not at fault. Probed at d9ea7a5b44 with a document whose root was already a horizontal split — preview 6px above the bottom border → previewToOperation → Operations.splitNode:
previewKind edge-bottom previewTarget split-root accepted
operation splitNode targetNodeId split-root orientation vertical
newRoot split-split-root-0 children [split-root, tabs-gamma-0]
droppedIsDirectRootChild TRUE
Full width, exactly as previewed. Operations.splitNode wraps the root (if (!parentSlot) doc.root = newSplitId), so nesting order is docking order.
The defect is in src/dashboard/dock/interaction/DragAffordances.mjs#ensureGeometry:
rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone'
? (WorkspaceDocument.getZoneNodeId(nodes[…].zones?.center) ?? me.owner.dockModel.root)
: me.owner.dockModel.root;When the root is an edge-zone, the chips target its center. splitNode then writes back through setZoneNodeId(parent, 'center', newSplit) — nesting the drop among the side zones, which are its siblings. Meanwhile the chip renders inset at the host rect's edge midpoints.
⭐@neo-gpt-emmy's statement of the invariant is the precise one: "the root chip's nodeId and its rectangle must describe the same dockable boundary. A center nodeId paired with the whole-host rect is the defect." Preview and commit disagree because they are measured against different nodes.
Provenance: the retarget arrived in e9f10ba17e (#17837/#17841), a vocabulary/anatomy refactor — not a fix for a known defect.
The architectural decision
Two corner rules currently coexist, switching on a node type the user cannot see:
| root type |
rule |
| split tree |
WPF DockPanel — docking order owns the corner |
edge-zone |
Swing BorderLayout — fixed slots, structure owns it |
@tobiu's stated preference — drop bottom → full width; later drop right → right takes full height and bottom narrows — is the DockPanel rule, and the split-tree path already implements it. Decision: keep last-drop-wins, and make it uniform. Three-way agreement (@neo-opus-vega, @neo-gpt, @neo-gpt-emmy).
🔴But "always use document.root" is wrong, and @neo-gpt-emmy's probe found why the conditional exists. On the canonical vessel shape (edge-zone shell → center stack), wrapping the whole shell leaves resolveStackRoot = null. That is live consumed authority — LayoutAdapter.project reads it under enableStackDrag, and Participation rejects whole-stack admission unless it matches groupNodeId. Splitting the center preserves the shell and returns the new split as the stack root.
So one node kind conflates two things: a real arrangement root, and a shell around transferable content. The fix is to resolve an explicit dockable-root boundary from the owner/host contract and measure that node for both preview and commit — never to delete the conditional.
Evidence already gathered
- @neo-opus-vega: split-tree root chain correct end to end (above).
- @neo-gpt at
d3b7f0c2: edge-zone with center + left bands — splitNode(bottom, target=frame) → V[frame,tabs-b], then splitNode(right, target=currentRoot) → H[previousRoot,tabs-c]. Both validate, frame descriptor byte-identical, JSON round-trip preserves nested child order.
- @neo-gpt-emmy, three probes: the real dense Workstation
initialDocument wraps correctly and createSavedLayout → JSON → restoreSavedLayout preserves the document byte-for-byte (this answers the saved-layout ordering concern — persistence stores the resulting tree, not a drop replay); the vessel resolveStackRoot regression above; and a sole-pane case where wrapping leaves an empty old edge-zone sibling, since normalizeTree retains empty edge-zones.
Not yet covered by any probe: the real producer→preview→commit path, rendered geometry across two sequential drops, and Group window materialization.
Acceptance criteria
Out of scope
Renderer/CSS sizing compensation, new callback or config layers (no probe indicated one), #18414's placeholder work, the human-overlap matrix, and any change to the edge-zone internal fixed-slot layout — the shell keeps its own arrangement.
Avoided traps
Do not "fix" the report by shrinking the Workstation root preview to match the center — that hides the mismatch instead of resolving which boundary is authoritative. Do not delete the conditional outright (breaks resolveStackRoot). Do not introduce a second projection path or a drop-order replay in persistence.
Decision Record impact: aligned-with ADR 0029 — this states an invariant the drop model already implies rather than amending authority.
Related
Related: #18303 · #18304 · #17837
Live latest-20 open sweep plus a 12-message A2A recency scan run immediately before creation; no equivalent or competing claim. Origin Session ID: 9b1a4410-2864-4d83-9b7b-105972029354
Context and problem
Reported by @tobiu on merged
dev: dock layouts offer a 5-arm cross inside a container and 4 chips at the root edges. The root top/bottom dropzones span the full width and the sides the full height — but dropping at the bottom inapps/workstationdoes not produce what the preview drew. The side zones win and the dropped zone comes out narrower.His framing is the right one: "a classic border-layouts problem — which corners get more weight."
The mechanism, located
The semantic model is not at fault. Probed at
d9ea7a5b44with a document whose root was already a horizontal split — preview 6px above the bottom border →previewToOperation→Operations.splitNode:Full width, exactly as previewed.
Operations.splitNodewraps the root (if (!parentSlot) doc.root = newSplitId), so nesting order is docking order.The defect is in
src/dashboard/dock/interaction/DragAffordances.mjs#ensureGeometry:rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone' ? (WorkspaceDocument.getZoneNodeId(nodes[…].zones?.center) ?? me.owner.dockModel.root) : me.owner.dockModel.root;When the root is an
edge-zone, the chips target its center.splitNodethen writes back throughsetZoneNodeId(parent, 'center', newSplit)— nesting the drop among the side zones, which are its siblings. Meanwhile the chip renders inset at the host rect's edge midpoints.⭐@neo-gpt-emmy's statement of the invariant is the precise one: "the root chip's nodeId and its rectangle must describe the same dockable boundary. A center nodeId paired with the whole-host rect is the defect." Preview and commit disagree because they are measured against different nodes.
Provenance: the retarget arrived in
e9f10ba17e(#17837/#17841), a vocabulary/anatomy refactor — not a fix for a known defect.The architectural decision
Two corner rules currently coexist, switching on a node type the user cannot see:
DockPanel— docking order owns the corneredge-zoneBorderLayout— fixed slots, structure owns it@tobiu's stated preference — drop bottom → full width; later drop right → right takes full height and bottom narrows — is the DockPanel rule, and the split-tree path already implements it. Decision: keep last-drop-wins, and make it uniform. Three-way agreement (@neo-opus-vega, @neo-gpt, @neo-gpt-emmy).
🔴But "always use
document.root" is wrong, and @neo-gpt-emmy's probe found why the conditional exists. On the canonical vessel shape (edge-zoneshell → center stack), wrapping the whole shell leavesresolveStackRoot = null. That is live consumed authority —LayoutAdapter.projectreads it underenableStackDrag, andParticipationrejects whole-stack admission unless it matchesgroupNodeId. Splitting the center preserves the shell and returns the new split as the stack root.So one node kind conflates two things: a real arrangement root, and a shell around transferable content. The fix is to resolve an explicit dockable-root boundary from the owner/host contract and measure that node for both preview and commit — never to delete the conditional.
Evidence already gathered
d3b7f0c2:edge-zonewith center + left bands —splitNode(bottom, target=frame)→V[frame,tabs-b], thensplitNode(right, target=currentRoot)→H[previousRoot,tabs-c]. Both validate, frame descriptor byte-identical, JSON round-trip preserves nested child order.initialDocumentwraps correctly andcreateSavedLayout → JSON → restoreSavedLayoutpreserves the document byte-for-byte (this answers the saved-layout ordering concern — persistence stores the resulting tree, not a drop replay); the vesselresolveStackRootregression above; and a sole-pane case where wrapping leaves an empty oldedge-zonesibling, sincenormalizeTreeretains empty edge-zones.Not yet covered by any probe: the real producer→preview→commit path, rendered geometry across two sequential drops, and Group window materialization.
Acceptance criteria
nodeIdand its rendered rect describe the same boundary; a drop commits against the node the preview measured.[oldRoot, newTabs]; a following right yields a horizontal root[previousRoot, newTabs]with the bottom nested one deeper. Asserting rendered widths alone is insufficient: a wrong tree passes if the renderer compensates.resolveStackRootstays non-null for the canonical vessel shape, andParticipationstill admits a whole-stack transfer after a root edge drop.edge-zonesibling, or the producer rejects the drop earlier — test the path rather than assumingsplitNodeeliminates the empty half.Out of scope
Renderer/CSS sizing compensation, new callback or config layers (no probe indicated one),
#18414's placeholder work, the human-overlap matrix, and any change to theedge-zoneinternal fixed-slot layout — the shell keeps its own arrangement.Avoided traps
Do not "fix" the report by shrinking the Workstation root preview to match the center — that hides the mismatch instead of resolving which boundary is authoritative. Do not delete the conditional outright (breaks
resolveStackRoot). Do not introduce a second projection path or a drop-order replay in persistence.Decision Record impact:
aligned-withADR 0029 — this states an invariant the drop model already implies rather than amending authority.Related
Related: #18303 · #18304 · #17837
Live latest-20 open sweep plus a 12-message A2A recency scan run immediately before creation; no equivalent or competing claim. Origin Session ID: 9b1a4410-2864-4d83-9b7b-105972029354