Context
PR #14974 review cycle 2 hardened neo.harness.dockCandidates.v1 after the original fail-closed review. The exact-head re-review at 2dddcd5e8e909ceb8ebe19e0e6c35a25bcf81ed2 independently confirmed the real producer path and all binding gesture paths, then ran one malformed-payload falsifier against the new validator.
The falsifier mutates an otherwise valid top candidate to {kind: 'split-before', orientation: 'horizontal'}. isValidCandidateSet() returns true, even though a top/bottom sibling insertion is valid only on the vertical axis. The real producer does not emit this mismatch, so this is a narrow contract-hardening successor rather than a blocker for PR #14974.
The Problem
crossKindMatchesPosition(position, kind) verifies leading/trailing direction but does not receive or validate placement.orientation. Consequently:
- top/bottom
split-before|split-after candidates can claim horizontal;
- left/right
split-before|split-after candidates can claim vertical;
- the indicator can visually advertise one direction while
previewToOperation() commits a perpendicular split.
That contradicts the contract's fail-closed promise for malformed or lying menus. The bug is not reachable through the current DockPreviewProducer.produceCandidates() implementation, which derives kind and orientation together, but it remains reachable through any malformed consumed payload.
Empirical reproducer at PR #14974 head:
const top = set.cross.find(candidate => candidate.position === 'top');
top.preview.placement = {kind: 'split-before', orientation: 'horizontal'};
isValidCandidateSet(set); The Architectural Reality
The ownership is already correct:
src/dashboard/dockPreviewContract.mjs owns structural validation for neo.harness.dockCandidates.v1.
src/dashboard/DockPreviewProducer.mjs owns grammar-correct candidate production and needs no behavioral change.
previewToOperation() remains the semantic conversion path and must not compensate for an invalid candidate set.
DockDropIndicators remains a commit-free renderer/selector.
The validator should express the same axis rule that the producer already implements: top/bottom sibling insertion requires vertical; left/right sibling insertion requires horizontal.
The Fix
Extend the candidate-set validation helper in src/dashboard/dockPreviewContract.mjs so split candidates validate direction, kind, and orientation as one semantic tuple.
Add focused negative tests beside the existing malformed-menu probes in test/playwright/unit/dashboard/DockPreviewProducer.spec.mjs. Preserve the producer, renderer, reducer, and public schema version.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
isValidCandidateSet() split-candidate tuple |
Dock choreography §06 grammar; ADR 0029 §2.3; #14959 Contract Ledger |
Accept top/bottom split-* only with vertical; accept left/right split-* only with horizontal |
Contradictory tuple returns false; indicator layer clears |
Tighten helper JSDoc to name the axis invariant |
Four negative axis probes plus producer-emission positive control |
Acceptance Criteria
Out of Scope
Changing indicator geometry, the real pointer journey, Escape delivery (#14980), or the previewToOperation() mapping.
Decision Record impact
aligned-with ADR 0029 — this closes a fail-closed validation edge inside the existing presentation/runtime contract.
Related
#14959 · PR #14974 · #14980 · parent #13158.
Live latest-open sweep: checked the latest 20 open issues immediately before creation on 2026-07-10; no equivalent found. A2A herd-window sweep: checked the latest 30 all-state messages; no competing claim or in-flight ticket on this validator edge.
Origin Session ID: 019f484c-662f-7f31-969a-cbde373efd4a
Retrieval Hint: "dockCandidates split orientation direction tuple fail closed top horizontal"
Context
PR #14974 review cycle 2 hardened
neo.harness.dockCandidates.v1after the original fail-closed review. The exact-head re-review at2dddcd5e8e909ceb8ebe19e0e6c35a25bcf81ed2independently confirmed the real producer path and all binding gesture paths, then ran one malformed-payload falsifier against the new validator.The falsifier mutates an otherwise valid top candidate to
{kind: 'split-before', orientation: 'horizontal'}.isValidCandidateSet()returnstrue, even though a top/bottom sibling insertion is valid only on the vertical axis. The real producer does not emit this mismatch, so this is a narrow contract-hardening successor rather than a blocker for PR #14974.The Problem
crossKindMatchesPosition(position, kind)verifies leading/trailing direction but does not receive or validateplacement.orientation. Consequently:split-before|split-aftercandidates can claimhorizontal;split-before|split-aftercandidates can claimvertical;previewToOperation()commits a perpendicular split.That contradicts the contract's fail-closed promise for malformed or lying menus. The bug is not reachable through the current
DockPreviewProducer.produceCandidates()implementation, which derives kind and orientation together, but it remains reachable through any malformed consumed payload.Empirical reproducer at PR #14974 head:
const top = set.cross.find(candidate => candidate.position === 'top'); top.preview.placement = {kind: 'split-before', orientation: 'horizontal'}; isValidCandidateSet(set); // true; expected falseThe Architectural Reality
The ownership is already correct:
src/dashboard/dockPreviewContract.mjsowns structural validation forneo.harness.dockCandidates.v1.src/dashboard/DockPreviewProducer.mjsowns grammar-correct candidate production and needs no behavioral change.previewToOperation()remains the semantic conversion path and must not compensate for an invalid candidate set.DockDropIndicatorsremains a commit-free renderer/selector.The validator should express the same axis rule that the producer already implements: top/bottom sibling insertion requires
vertical; left/right sibling insertion requireshorizontal.The Fix
Extend the candidate-set validation helper in
src/dashboard/dockPreviewContract.mjsso split candidates validate direction, kind, and orientation as one semantic tuple.Add focused negative tests beside the existing malformed-menu probes in
test/playwright/unit/dashboard/DockPreviewProducer.spec.mjs. Preserve the producer, renderer, reducer, and public schema version.Contract Ledger
isValidCandidateSet()split-candidate tuplesplit-*only withvertical; accept left/rightsplit-*only withhorizontalfalse; indicator layer clearsAcceptance Criteria
split-before|split-aftercandidates are rejected unlessplacement.orientation === 'vertical'.split-before|split-aftercandidates are rejected unlessplacement.orientation === 'horizontal'.Out of Scope
Changing indicator geometry, the real pointer journey, Escape delivery (#14980), or the
previewToOperation()mapping.Decision Record impact
aligned-with ADR 0029— this closes a fail-closed validation edge inside the existing presentation/runtime contract.Related
#14959 · PR #14974 · #14980 · parent #13158.
Live latest-open sweep: checked the latest 20 open issues immediately before creation on 2026-07-10; no equivalent found. A2A herd-window sweep: checked the latest 30 all-state messages; no competing claim or in-flight ticket on this validator edge.
Origin Session ID: 019f484c-662f-7f31-969a-cbde373efd4a
Retrieval Hint: "dockCandidates split orientation direction tuple fail closed top horizontal"