Context
Found during the #17863 journey-repair diagnosis (its engine-escalation clause routes the fix here). The WorkstationDragAffordancesNL park journey asserts the top edge chip's position against the app header — and the chip is not merely mispositioned, it is never offered: the parked candidate set arrives with root: undefined, so all four container edge chips stay -off. Reproduced with an in-spec diagnostic dump: candidateSet.root: undefined, zone: 'scale-tabs' while the cross indicators light normally.
The Problem
Neo.dashboard.dock.interaction.DragAffordances#ensureGeometry resolves the chips' root target as:
rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone'
? (nodes[me.owner.dockModel.root].zones?.center ?? me.owner.dockModel.root)
: me.owner.dockModel.root;In the current document schema an edge-zone's zones.center is a descriptor object ({nodeId: 'split-main'} in Workstation's committed document), not a bare node-id string — Document.getZoneNodeId exists precisely to unwrap both shapes, and the projection tier uses it everywhere. Here the raw descriptor flows into geometry.root.nodeId, and PreviewProducer#produceCandidates fail-closes on its type guard (typeof root.nodeId === 'string') → rootOut = null → DropIndicators receives no root.chips → every edge chip keeps neo-dashboard-dock-drop-indicator-off.
Blast radius: every edge-zone-rooted consumer (Workstation, the cockpit) silently loses the dock-to-container-edge affordance family during drags. Split-rooted documents (the dashboard dock example, Demo-A) take the : me.owner.dockModel.root branch with a bare string and are unaffected — which is why the defect hid: the affordance demos kept working while the flagship lost its chips.
The Architectural Reality
src/dashboard/dock/interaction/DragAffordances.mjs — the resolution site; imports PreviewProducer but not Document.
src/dashboard/dock/model/Document.mjs#getZoneNodeId — the canonical object-descriptor reader ({nodeId, …}); the v13.2 contract deliberately rejects the retired bare-string shorthand (fail-closed null, no hidden compatibility reader — its own JSDoc states this). (Corrected per PR #17867 review, 2026-08-29 — the original line claimed dual-shape support.)
src/dashboard/dock/interaction/PreviewProducer.mjs#produceCandidates — the fail-closed consumer whose guard converts the malformed id into silent chip absence. The guard is CORRECT; the producer's silence is the designed fail-closed posture.
The Fix
Unwrap through the canonical helper at the resolution site:
rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone'
? (Document.getZoneNodeId(nodes[me.owner.dockModel.root].zones?.center) ?? me.owner.dockModel.root)
: me.owner.dockModel.root;plus the Document import. Unit witness: a DragAffordances geometry test with an edge-zone document whose center is descriptor-shaped must produce a candidate set whose root.nodeId is the unwrapped string and whose chips carry all four edges — red against the current line by construction.
Acceptance Criteria
Out of Scope
- The journey spec itself (#17863 owns spec-tier repairs; its park test needs no change — it greens when chips return).
- Any indicator-menu visual/geometry change.
Decision Record impact
none — restores the shipped affordance contract; no authority change.
Related
- #17863 (the journey-repair lane whose escalation clause filed this; parent #13158)
- #17836 (epic)
Origin Session ID: c98e1ede-1a32-46f3-90ef-aaf37def487d
Retrieval Hint: "root edge chips descriptor center zone getZoneNodeId unwrap"
Context
Found during the #17863 journey-repair diagnosis (its engine-escalation clause routes the fix here). The
WorkstationDragAffordancesNLpark journey asserts the top edge chip's position against the app header — and the chip is not merely mispositioned, it is never offered: the parked candidate set arrives withroot: undefined, so all four container edge chips stay-off. Reproduced with an in-spec diagnostic dump:candidateSet.root: undefined, zone: 'scale-tabs'while the cross indicators light normally.The Problem
Neo.dashboard.dock.interaction.DragAffordances#ensureGeometryresolves the chips' root target as:rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone' ? (nodes[me.owner.dockModel.root].zones?.center ?? me.owner.dockModel.root) : me.owner.dockModel.root;In the current document schema an edge-zone's
zones.centeris a descriptor object ({nodeId: 'split-main'}in Workstation's committed document), not a bare node-id string —Document.getZoneNodeIdexists precisely to unwrap both shapes, and the projection tier uses it everywhere. Here the raw descriptor flows intogeometry.root.nodeId, andPreviewProducer#produceCandidatesfail-closes on its type guard (typeof root.nodeId === 'string') →rootOut = null→DropIndicatorsreceives noroot.chips→ every edge chip keepsneo-dashboard-dock-drop-indicator-off.Blast radius: every edge-zone-rooted consumer (Workstation, the cockpit) silently loses the dock-to-container-edge affordance family during drags. Split-rooted documents (the dashboard dock example, Demo-A) take the
: me.owner.dockModel.rootbranch with a bare string and are unaffected — which is why the defect hid: the affordance demos kept working while the flagship lost its chips.The Architectural Reality
src/dashboard/dock/interaction/DragAffordances.mjs— the resolution site; importsPreviewProducerbut notDocument.src/dashboard/dock/model/Document.mjs#getZoneNodeId— the canonical object-descriptor reader ({nodeId, …}); the v13.2 contract deliberately rejects the retired bare-string shorthand (fail-closednull, no hidden compatibility reader — its own JSDoc states this). (Corrected per PR #17867 review, 2026-08-29 — the original line claimed dual-shape support.)src/dashboard/dock/interaction/PreviewProducer.mjs#produceCandidates— the fail-closed consumer whose guard converts the malformed id into silent chip absence. The guard is CORRECT; the producer's silence is the designed fail-closed posture.The Fix
Unwrap through the canonical helper at the resolution site:
rootId = nodes[me.owner.dockModel.root]?.type === 'edge-zone' ? (Document.getZoneNodeId(nodes[me.owner.dockModel.root].zones?.center) ?? me.owner.dockModel.root) : me.owner.dockModel.root;plus the
Documentimport. Unit witness: a DragAffordances geometry test with an edge-zone document whosecenteris descriptor-shaped must produce a candidate set whoseroot.nodeIdis the unwrapped string and whosechipscarry all four edges — red against the current line by construction.Acceptance Criteria
centerzone is descriptor-shaped, the produced candidate set carriesroot.nodeIdas the center node's id string and four edge chips.nullfalls back to the document root, and no legacy compatibility reader is added. (AC corrected per PR #17867 review, 2026-08-29.)WorkstationDragAffordancesNLpark journey's top-chip guard executes again (validated in the #17863 lane; its AC converts from deferred-evidence once this merges).Out of Scope
Decision Record impact
none — restores the shipped affordance contract; no authority change.
Related
Origin Session ID: c98e1ede-1a32-46f3-90ef-aaf37def487d Retrieval Hint: "root edge chips descriptor center zone getZoneNodeId unwrap"