134 lines
7.6 KiB
Markdown
134 lines
7.6 KiB
Markdown
# Fresh ordinary saved-view selection review — 2026-09-23
|
||
|
||
Independent static correctness review of the local, uncommitted feature on
|
||
`codex/multiselect-9e8r6t-20260923`, based on
|
||
`b8685416f56e53e30bd11ecf93a54cea10ac8458`. Scope is ordinary saved views only,
|
||
against [the implementation contract](multiselect-contract-20260923.md).
|
||
State, Allocation, Process/Scenario adapters and aggregate model deletion are
|
||
known incomplete work and are not findings in this review. The pure core
|
||
movement planner has its [separate review](multiselect-core-review-20260923.md).
|
||
|
||
Reviewed the selection reducer, App adapter, canvas adapter and gesture, focus
|
||
owner, App/menu/view integration, selection and focus tests, and adjacent
|
||
selection/sync/route/navigation paths. The initial review did not edit production
|
||
or test source. The parent subsequently delegated the bounded correction below.
|
||
No test execution, builds, GUI operation, Gitea update or publication were
|
||
performed by this reviewer.
|
||
|
||
## Findings
|
||
|
||
### P2 — starting passive I/O loses Shift that was already held
|
||
|
||
`crates/oa-desktop/src/diagram/focus.rs:93–97` replaces the wrapped canvas tree
|
||
with `Tree::new` when suspension starts. This intentionally retires pointer
|
||
gestures, but also resets the newly added `State.modifiers`
|
||
(`diagram.rs:69–72`) to its default. The only normal assignment of a nonempty
|
||
modifier state is receipt of `ModifiersChanged` (`diagram.rs:615–617`), and
|
||
the toggle decision reads that retained state (`diagram/multiselect.rs:208`).
|
||
The wrapping view supplies `app.busy` as suspension (`views.rs:511–516`).
|
||
|
||
Concrete event path: select A; deliver `ModifiersChanged(SHIFT)` while a clean
|
||
Inspector field owns focus; start Save/export through a mouse action while
|
||
continuing to hold Shift; let busy become true and then false; click B without
|
||
an intervening modifier transition. The tree replacement discards Shift, so
|
||
the click follows the plain selection path and replaces A with B instead of
|
||
adding B. The same loss occurs if the canvas owned focus before I/O. No stale
|
||
model mutation is needed to trigger it.
|
||
|
||
The existing test
|
||
`mounted_suspended_modifiers_are_passive_and_survive_until_resumed_click`
|
||
(`app/diagram_focus_tests.rs:329`) sends Shift **after** setting busy and
|
||
settling, so it verifies a different ordering and cannot catch this path.
|
||
|
||
Retain passive modifier state across gesture retirement, or restore it from
|
||
an owner whose lifetime survives the canvas replacement before the first
|
||
resumed press. Keep the existing I/O pointer-retirement behavior. Add the
|
||
mounted replay with Shift delivered before suspension, no modifier events
|
||
during suspension/resumption, and assert A/B selection, unchanged model and
|
||
history, and no keyboard focus granted by modifier tracking. This finding is
|
||
established by the explicit state-reset path; runtime reproduction remains
|
||
for the owning session's affected gate.
|
||
|
||
No other new in-scope correctness finding was established.
|
||
|
||
## Rechecked protections
|
||
|
||
- `Selection::matches` combines document/surface/revision/epoch, membership,
|
||
primary and monotonic generation. The App rejects stale operation envelopes
|
||
before allocation and camera boundary side effects. Choose/Clear use context
|
||
admission; movement, Hide, refusal and collapse require a captured snapshot.
|
||
- Full typed-set admission prevents silently dropping edges or ports from a
|
||
group movement/Hide request. Exact visibility is rechecked at execution.
|
||
Reconciliation now preserves surviving targets when the exact primary
|
||
disappears, and does not substitute a new placement of the same semantic
|
||
element. The Placement-properties identity correction is present in
|
||
`legacy_target`.
|
||
- A selected-member press defers primary/Inspector changes. Its stationary
|
||
release collapses even a mixed group; a threshold-crossing mixed drag
|
||
refuses while retaining membership. Threshold crossing remains latched for
|
||
cancellation, zero movement and return to origin. The latest
|
||
`Press::viewport_matches` delegates to the underlying gesture, so its retired
|
||
state cannot regain authority.
|
||
- Ordered pointer normalization precedes group handling. Group preview and
|
||
release consume the same captured geometry and planner. Active groups
|
||
consume keyboard input; Escape cancels before a subsequent Escape clears.
|
||
Middle pan excludes group admission. Context, selection generation,
|
||
viewport and window/pointer lifecycle changes retire the gesture.
|
||
- Context-menu opening/dismissal preserves membership; Add/Remove uses the
|
||
captured target, and selected-member group menus expose aggregate Hide and
|
||
explicit inspection/collapse. App guards prevent queued singleton Move,
|
||
diagram removal or model Delete from acting on one member of a new group.
|
||
The legacy Hide action redirects to the whole visible set.
|
||
- Ordinary passive modifiers are forwarded without acquiring keyboard focus,
|
||
including while another mounted field owns focus. Mounted tests cover this
|
||
primary requirement and unfocused command denial. The finding above concerns
|
||
the separate tree-replacement lifetime.
|
||
- The earlier unconditional global reconciliation cost is removed. Boundary
|
||
changes trigger reconciliation and projection reuses `document.view_index()`.
|
||
No latency or scale-performance measurement is claimed here.
|
||
|
||
## Acceptance limits
|
||
|
||
The parent reports desktop-3 passed 1,254 tests with zero failures and six
|
||
ignored on its recorded 729-file input set, followed by the small
|
||
`Press::viewport_matches` change. That report was context for this review;
|
||
this reviewer did not rerun or independently certify it. The parent owns the
|
||
affected gate, core/native checker result, native interaction verification and
|
||
integration acceptance. This static review cannot establish actual Windows
|
||
focus behavior, visual clarity, save/reopen usability, or performance.
|
||
|
||
The review inspected `diagram/multiselect.rs` with SHA-256
|
||
`C8ADF20046176F620CAEF52DD8ED93EEDB349F72D8C9F50401AAA440848803B4`,
|
||
`diagram/focus.rs` with
|
||
`16AA9C6254BE7E30C67A71C0D5FE5FC763F40923912154209907D0FA17E75889`, and
|
||
`app/multiselect.rs` with
|
||
`D3CA4A2D593CB15CEDD40897BBCCF410111D9E54AD8ADFE9AD985B47611B0B0B`.
|
||
These identify local reviewed contents, not a published commit.
|
||
|
||
## Delegated correction after review
|
||
|
||
The parent authorized edits only to `diagram/focus.rs`,
|
||
`app/diagram_focus_tests.rs` and this note. The focus owner now stores passive
|
||
modifier state independently of the child canvas and marks it for restoration
|
||
when suspension replaces the child. Before the next admitted child event it
|
||
replays `ModifiersChanged` without acquiring keyboard focus. A genuine modifier
|
||
change updates that retained state and window unfocus clears it. The existing
|
||
subtree replacement, pointer retirement, suspended-input rejection and keyboard
|
||
ownership checks are preserved.
|
||
|
||
Two mounted regression tests were added in source:
|
||
|
||
- `mounted_shift_held_before_suspension_survives_without_another_modifier_event`
|
||
holds Shift with Inspector focus before I/O, crosses busy/resume without a
|
||
modifier transition, and verifies that the next click selects A/B without
|
||
model/history/draft changes or passive focus acquisition.
|
||
- `mounted_shift_release_or_window_unfocus_during_suspension_does_not_restore_stale_shift`
|
||
covers release and window-unfocus while busy, then another suspension cycle
|
||
to prove the retained owner state was updated rather than restoring stale
|
||
Shift. The resumed click must select B alone with unchanged model/history.
|
||
|
||
The correction and test source were statically inspected; the parent owns
|
||
compilation, execution and acceptance. The P2 above describes the pre-correction
|
||
path and remains the regression rationale, not a claim that the new source
|
||
still has that path.
|