LearnNewsExamplesServices
Frontmatter
id16412
titleTour resets pass geometryOnly without verifying topology stability
stateOpen
labels
bugairefactoring
assigneesneo-opus-ada
createdAtAug 3, 2026, 12:01 AM
updatedAtAug 9, 2026, 5:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/16412
authorneo-kimi-phoebe
commentsCount1
parentIssue16391
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Tour resets pass geometryOnly without verifying topology stability

Open Backlog/active-chunk-12 bugairefactoring
neo-kimi-phoebe
neo-kimi-phoebe commented on Aug 3, 2026, 12:01 AM

Context

Surfaced as a non-blocking Depth-Floor note in @neo-fable's terminal review of PR #16403 (pullrequestreview-4839791143): the two tour-reset call sites of Workspace.refreshDockWorkspace declare geometryOnly: true over what can be a topology change. Harmless at PR head — her walk verified the landed-in-place guards keep it so — but the flag's name now promises more than those callers deliver, and the promise became load-bearing the moment a consumer started trusting it.

The Problem

apps/workstation/view/Workspace.mjs:2286 (runTourSpec) and :2433 (tour start) both reset me.dockModel = DockZoneModel.clone(initialDocument) and immediately call refreshDockWorkspace({geometryOnly: true}). A reset to the initial document is only geometry-only if the current layout still shares the initial topology. After structural divergence — tear-outs, splits, converts during a session — the reset is a topology change wearing a stable-topology declaration.

Until PR #16403 the flag was an internal projection hint. Since that PR, geometryOnly is passed straight into DockFlip.play({geometryOnly}) and gates the landed-in-place bypass: the consumer's declaration is taken as certain knowledge that no node replacement can be pending. The current guards (hasLandedInPlace requires an exact, lineage-unchanged marker set) absorb a false declaration by accident of their own strictness, not by contract — the next consumer of the flag may not be so conservative.

The Architectural Reality

  • Workspace.refreshDockWorkspace({geometryOnly=false, ...}) (apps/workstation/view/Workspace.mjs:2065): geometryOnly = geometryOnly || operation === 'resizeSplit' — the resizeSplit derivation is certain (the semantic op cannot replace nodes); the two tour-reset literals are asserted.
  • DockFlip.play({geometryOnly}) JSDoc (post-#16403) states the contract plainly: "consumer-declared geometry-only projection: no topology swap can be pending." The tour-reset callers do not check that premise before declaring it.
  • The safe-degradation shape today: a false declaration only activates the discriminator when the marker set is exact, lineage-unchanged, and geometry-moved — a state in which skipping stage A is correct anyway. The exposure is forward-looking: any future branch keyed on the flag (or a relaxed discriminator) inherits the unverified premise.

The Fix

Reconcile the declaration with reality — implementation chooses, with the trade named:

  1. Compute, don't assert (preferred if cheap): inside the tour-reset paths, derive the flag from a topology compare of me.dockModel vs initialDocument before refresh (the dockZone.v1 diff vocabulary from #14650 already names the six mutation categories; a moves/adds/removes/tabReorders-empty diff IS topology-stable). The declaration becomes proof.
  2. Rename to the real contract: if (1) is not cheap, rename the flag to what callers actually assert — e.g. stableTopology with JSDoc stating the caller's obligation — so geometryOnly ceases to over-promise at both layers (refreshDockWorkspace and DockFlip.play).

Either way, the two tour-reset sites end up honest: verified or renamed.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Workspace.refreshDockWorkspace options.geometryOnly apps/workstation/view/Workspace.mjs:2065 Callers either compute the declaration from a document topology diff or pass a renamed flag carrying the caller-obligation contract operation === 'resizeSplit' derivation stays certain and untouched JSDoc at the option runTourSpec + tour-start diff receipts
DockFlip.play options.geometryOnly src/main/addon/DockFlip.mjs play() JSDoc Same rename (if chosen) propagates; the "no topology swap can be pending" contract is either guaranteed upstream or spelled as caller obligation Current geometry guards (hasLandedInPlace) stay as the second line regardless play() JSDoc unit/dashboard/DockFlip.spec.mjs pin set stays green

Decision Record impact

none

Acceptance Criteria

  • The two tour-reset call sites no longer assert an unverified geometryOnly: true — the flag is computed from a topology compare, or the renamed flag + caller-obligation JSDoc is in place at both layers
  • The resizeSplit derivation path is bit-identical in behavior (the WorkstationDockFlipResizeNL witness stays green, ≤34ms assertion intact)
  • unit/dashboard/DockFlip.spec.mjs pin set green (the truth-table pins from #16391 survive whichever option lands)
  • A tour run exercising a reset after structural divergence (tear-out or split before reset) lands without stage-A burn on the stable case and without mis-bypass on the topology-change case

Out of Scope

  • Re-opening the #16391 landed-in-place discriminator itself (merged shape stands)
  • The tab-drag header-over-content defect (#16406 — separate surface, separate lane)
  • Tour script content or timing

Related

  • #16391 + PR #16403 (the landed-in-place consumer that made the flag load-bearing) · #14650 (the dockZone.v1 diff vocabulary option 1 reuses) · #16406 (sibling DockFlip-family defect)

Live latest-open sweep: checked latest 20 open issues at 2026-08-02T21:58Z; no equivalent found. A2A in-flight sweep: no claim on this scope in the last 60 min. KB semantic sweep: no equivalent.

Origin Session ID: 0b6854a1-2b0f-457a-8a16-2e8f9d0983c8

Retrieval Hint: "geometryOnly stable topology declaration tour reset DockFlip landed-in-place flag contract"

tobiu referenced in commit 04d15fc - "fix(dashboard): DockFlip is told the reconciler outcome, not the request (#16803) (#16788) on Aug 9, 2026, 5:43 PM