20 KiB
Scenario group adapter design against the combined source
Read-only design review for issue 29, 2026-09-24. Inspected the actual combined source in C:/ar-scenario-9e8r6t while the parent's frozen planner gate runs. This report is the only written file. No code/tests, Cargo, Git, network, or GUI operations. References below are repository-relative current-source locations, not evidence of runtime acceptance. The parent owns synthesis, integration and publication.
Recommendation
Reuse the existing shared Selection/Snapshot data structure, but deliver Scenario selection and semantic reorder through a Scenario-specific event envelope containing the full scenario::Context. Map the shared selection surface to the actual representation: Derived => Surface::Scenario(owner); Saved => Surface::Saved(diagram). Keep Representation and visit in the Scenario envelope and captured gesture/menu; do not extend every generic Context or change diagram-usage Surface globally.
Preserve the current saved-view default interaction: header/message drags mean authored spacing. Add an explicit, clearly labelled one-shot Reorder selected occurrences action for semantic group reorder in a saved representation. It arms only the accepted exact selection for the next drag and visibly explains that shared order changes while saved positions remain. Derived Select can retain its ordinary semantic-reorder behavior. A saved group body drag without that explicit action must never silently reorder, move only its primary, or infer group spacing: retain the set and explain the unavailable group spacing operation after threshold crossing. Singleton saved spacing continues through its existing controller.
This is a recommended bounded product/design decision, not an already accepted user requirement. It follows the existing distinction between saved presentation edits and semantic order, avoids forcing a representation switch merely to move a group, and preserves the existing default gesture.
What the combined source actually does
- Shared selection has no active Scenario adapter yet.
app/multiselect.rs:91boundary includes document/revision/page/active_diagram/legacy selected/placement, State identity and Process context/address.:105returns Process or State contexts on Behavior; it never returns a Scenario selection context.:122visibility similarly handles Process and projected saved Architecture/State;:223reconciliation falls into those same branches.diagram_selection.rs:141reconciles by Scope and exact visibility, while:179matches full generic context, generation, ordered membership and primary.Surface::ScenarioandTarget::ScenarioObjectexist as diagram-usage identities, but their existence does not wire live group input. - Scenario already has the missing lifetime authority.
behavior/scenario.rs:117Context contains document, owner, exact Representation, visit, revision and epoch.:391enter_representation generates a fresh visit on representation change or reentry;:422leave invalidates it. Thus A -> B -> A gets a different visit even when the Scenario owner and final saved diagram are the same.app.rs:2073constructs the authoritative current context.app/scenario_diagram.rs:6entry updates active_diagram and canvas_epoch, while reconciliation retains an invalid Saved identity rather than silently switching to Derived. - The top-level update order matters.
app.rs:1374rejects generic selection envelopes early.:1483routes Spacing directly to its handler and returns;:1501rejects stale Scenario document/owner/representation/visit before reveal/allocation/camera reconciliation. Only specified passive events relax epoch.:1553onward can retire navigation/gesture state, and:1603performs shared selection reconciliation after ordinary dispatch. New Scenario group packets must reject both the full Scenario envelope and shared snapshot before these side effects. Adding only an inner-handler check is too late. - Presentation is the actual visibility authority.
scenario/workspace.rs:78obtains either frozen spacing paint orpresentation::presentfor the exact representation. At:138it configures the actual Scene, coordinate capability and full spacing stamp. A saved view can display fallback/unavailable geometry with different capabilities. Do not useScene::sequenceor ancestor pointers as current saved-view visibility; those are derived semantic geometry and can authorize a target absent from the actual scene. Add a small Scenario accessor returning the visible typed hit targets from the exact displayed Scene. Keep visibility separate from reorder eligibility so lifelines, operands and mixed sets can be selected/inspected and then refused as a complete movement set. - Input currently knows only a singleton.
behavior.rs:1264SequenceInput has no modifier state or shared Snapshot.:1290captures one occurrence/parent;:2293releases either Select or singleton Reorder. Scene keeps rows/slots plus boxes/lines/activation/lifecycle primitives.:1841hit_item deliberately resolves marker, topmost titled box/header, exact line, activation's Call, label, then tolerant items and lanes. Reuse this exact winner for Shift and menu selection; do not build a second hit order.:1807picks the nearest slot first and only then checks parent; preserve that policy and expose a reason instead of choosing a farther legal slot. - Saved spacing already owns precise roles.
behavior.rs:2149branches on Saved before semantic reorder. Only LifelineHeader and MessageBody/MessageLabel start spacing; lifecycle markers, activation segments, guides, fragments and other boxes select only.scenario/spacing/controller.rs:17Stamp is full Context plus camera_epoch; sessions additionally own token, initial screen/scale/pan and frozen model.app/scenario_spacing.rs:20checks context, camera and active token before any mutation. Do not broaden these role permissions merely because a target is in a group. - Passive modifier support is already delivered by the wrapper.
behavior/canvas_workspace/viewport.rs:152observes ModifiersChanged before keyboard focus gates and forwards it to the child;:361replays held modifiers before a pointer press, including after child rebuild. Scenario currently ignores it except Delete's event modifiers. Add a local modifier field updated before gesture admission, clear on window unfocus, and never publish a focus/camera command for passive modifier events. The existing mounted regression isscenario/workspace/tests.rs:7. - Other controls still bypass group intent.
scenario/workspace.rs:913recursively builds order rows whose labels emit rawMessage::Select; arrows at:939emit rawMessage::Command(MoveStep).app.rs:2277handles singleton Remove;:2511singleton Reorder;:3557applies raw commands (including MoveStep) after apply(). Delete has a shared group guard at:3605, but its non-Process message offers Hide, which is inappropriate for Scenario occurrences. All need Scenario-aware handling before a displayed group can exist. - Menus are not yet Scenario-capable.
model_menu.rs:11Request has canvas/process but no Scenario context.:112target_matches rejects selection targets without those contexts and does not admit ScenarioObject.:171Actions-trigger captures Process/State context only.:264selection menu rows publish generic packets and treat non-Process groups as placements. Merely populating diagram_selection would therefore make the existing Actions trigger fail or offer wrong actions; wire the menu deliberately.
Lifetime alternatives
| Candidate | Concrete implementation | Tradeoff and decision |
|---|---|---|
| Extend generic Context/Scope globally | Add optional Scenario representation/visit to Context, or introduce a new generic SelectionSurface variant that holds them, then update every constructor/matcher/test in Diagram, State, Process and menus. | Can express everything, but pushes a Scenario-only lifecycle into unrelated adapters and conflates usage identities with input visits. No current requirement justifies that migration. Reject for this increment. |
| Scenario event envelope plus existing generic Snapshot | CanvasEvent::Selection { context: scenario::Context, event: ScenarioSelectionEvent }; mutation events carry the existing immutable Snapshot. A Scenario conversion maps actual representation to an existing Surface. Match full context and Selection::matches before side effects. |
Reuses the already authoritative visit mechanism, needs only a bounded Scenario adapter and small dispatch branches, preserves existing generic invariants. Preferred. |
| Owner-only Scope plus canvas_epoch alone | Convert full Scenario Context to Surface::Scenario(owner) and trust epoch to reject navigation; use bare generic selection messages. |
Epoch normally changes today, but representation identity is erased, surface changes fail to clear membership, and a future/passive path can weaken the guarantee. It also leaves menus without full visit admission. Reject as an implicit assumption. |
The recommended Scope mapping means changing saved A to saved B or Automatic clears the set, as required by the current multiselect contract's “changing surface clears the set.” Legacy single-object inspection can survive a same-owner representation switch, as it already does, then seed at most one visible target on the new surface. Do not silently preserve the whole group across representation navigation. This corrects a tempting owner-scoped shortcut: Scene representation is a distinct rendered surface even when semantic membership happens to match.
Keep visit out of Selection scope so routine context refreshes do not redefine logical membership. The full Scenario envelope is still mandatory: after A -> B -> A, an old A packet cannot pass even if the user recreated identical membership/primary and the shared surface equals A. Clear selection on leave/navigation under the existing contract; a new active visit also retires any gesture, menu or explicit reorder arm.
Minimal integration shape
- Add
scenario_contextconversion in multiselect.rs, with Scenario dispatch before the generic Saved projected-view branch. Includeself.scenario_context()in multiselect_boundary (or the specific owner/representation/visit identity plus existing revision). Route Scenario visibility, reconciliation and primary inspection to a boundedapp/scenario_multiselect.rsmodule, patterned after process_multiselect without importing qualified Process semantics. - On reconcile, preserve an existing exact group when legacy
selectedstill names its primary and the representation Scope is unchanged; prune only after ordinary accepted model changes. If primary disappears, choose the shared Selection's survivor; never retarget by definition. Explicit unrelated selection replaces the set. Inspection sets selected to the ScenarioObject ID, clears placement/bend/attachment/navigation focus as appropriate, refreshes the primary draft, and does not navigate representations. Keep property edits primary-only. - Scene gets a captured Scenario selection envelope and an immutable model Arc (or other existing accepted-document snapshot access) for pure Prepared construction. SequenceInput group state owns full context, Snapshot, pressed target, screen origin, bounds, pan/scale, camera_epoch, threshold-crossed flag, Prepared Result, last exact Slot, and cached Plan/refusal. These fields are local to Scenario, not a new shared gesture framework.
- Shift toggles the exact hit and captures the event before either spacing or reorder starts. Plain unselected press replaces. Plain press on a selected member freezes the complete set and primary; stationary release collapses to that member; once threshold is crossed, refusal, cancel, origin return and selected-anchor Noop keep both unchanged. Do not inspect the pressed member before knowing the outcome, because Details layout changes can change hit geometry. Background clear can coexist with the existing background pan only if it does not capture the semantic hit path.
- For each changed nearest Slot, call the frozen Prepared planner once and cache its result; exact same parent/anchor identity is the cache key. Preview must show selected count, destination and no-change/refusal, not a singleton ghost that implies only the primary will move. Fragment interior slots can produce a deliberate operand refusal.
- At release, first compare full current Context (including representation/visit), shared Snapshot generation/membership/primary, exact current visible membership, camera lifetime, bounds and tool/arm state. Refuse before any reconcile can prune the old set. Recompute using the same captured project/selection or prove current model equals that accepted revision; execute one admitted command through Document only for Change. Do not use
apply()to silently apply pending forms during this operation. On success restore the original primary before shared reconciliation: App::execute atapp.rs:1033receives SetSteps' parent as the core selected result and calls sync; blindly trusting that returned primary would collapse or misinspect the group.syncdoes not itself call reconcile_multiselect, so restore/inspect immediately before the outer reconciliation. Noop bypasses execute and navigation/reveal retirement. - Add Scenario group envelope checks alongside the existing early checks in App::update, not just CanvasEvent::context. Any bare generic DiagramSelection packet while Scenario is active should refuse unless it entered through the full Scenario wrapper; otherwise generic Choose/Clear remains an unguarded representation/visit channel. This applies equally to menu-generated selection packets.
Saved spacing versus explicit semantic reorder
Two viable bounded choices were compared:
- Require switching to Automatic arrangement. Existing representation picker (
workspace.rs:210) already exposes it, Derived already reorders, and saved spacing remains untouched. This has the least new input mode but switching rendered surfaces must clear the group under the accepted selection contract, requiring reselection. Preserving a group automatically to make this shortcut convenient would need a contract amendment. It also changes the working view when the user only asked to change order. - Explicit one-shot semantic reorder in the current saved view (preferred). Show a selected-count action such as “Reorder selected occurrences”; on activation capture full Context + Snapshot and validate the whole planner baseline. Display “Drag a selected occurrence to change shared order; saved positions stay unchanged” and Cancel. Only this armed gesture can outrank saved spacing. Complete or cancel disarms it; changing selection, tool, view, visit, revision, focus/camera or pending state also disarms. The default remains Select / move messages and lifelines. This adds one bounded Scenario state rather than changing meaning based on group size or Shift (Shift remains selection only).
For singleton normal saved drags, preserve the existing header/message spacing precedence and exact hit-role checks. For a multi-selection normal saved drag, capture a selection-owned pending press: stationary release follows the ordinary collapse rule; threshold crossing reports “Moving saved positions as a group is unavailable; choose Reorder selected occurrences to change sequence order.” No spacing Begin should be dispatched for only its primary. Unsupported sets (including lanes, Branch headers, cross-parent members) remain selectable but the explicit reorder action refuses the complete set with the planner reason. Do not expand role eligibility for activation/lifecycle hits in the ordinary saved-spacing mode.
The scenario_spacing early return bypasses the outer reconciliation. Once group selection exists, its Click/Commit paths must deliberately reconcile/inspect accepted singleton identity or use the Scenario selection adapter, and unchanged/failed spacing must preserve the captured membership/primary. Capturing a selection Snapshot with the spacing interaction or refusing Begin when the live set is a group prevents a queued singleton spacing packet from becoming a partial group edit after a selection change. Full stamp/token alone does not include selection generation. Do not gratuitously reset spacing camera/preview ownership by routing its packets through ordinary navigation.
A simpler first implementation may choose option 1 explicitly, but must label group reorder as Automatic-view-only and require reselection; it cannot claim full saved-view group operation acceptance. The preferred option provides the intended semantic action without changing presentation records or the active representation.
Controls and menus that must be closed before enabling group input
- Add full Scenario Context (and group Snapshot where appropriate) to contextual Request and validate it in Opened::new/valid plus target_matches. Admit only exact ScenarioObject with matching object, no placement, and actual represented visibility. Actions-trigger and right-click should capture without replacing; Add/Remove/Inspect/Select-only publish Scenario-wrapped events. Group menu offers the semantic reorder action and explains aggregate removal/Hide limits; no placement Hide.
- Replace Scenario order-row arrows with captured Scenario actions, or disable them for groups and add an early refusal for stale
Message::Command(MoveStep)while a Scenario group is current. Raw index packets are not safe against changed order/visit; a captured parent and occurrence/destination identity is better even for singleton arrows. Row selection should be captured, not delayed rawMessage::Select; otherwise old Details rows can navigate after a representation switch. Group row arrows must never apply to one displayed member. - Mirror Process early singleton guards for Scenario Select/Remove/Reorder, including stale menu/Delete packets, before reveal/camera mutation. Normal allowed group selection changes arrive only in the new envelope. Existing aggregate removal is separate: reject group Remove/Delete with an accurate reason, not “Hide selected.” Keep singleton deletion's existing review.
- Guard new selection, arm and reorder against source/property/behavior drafts, modal/rename/busy/read-only state, coordinate-unavailable fallback and wrong tool. Allow safe visual selection only where existing capabilities permit; a fallback hit is not movement authorization. Keep open-definition navigation and representation changes explicit boundaries.
Discriminating acceptance before integration
- A/B/A same-owner saved visits: stale Choose, Clear, Collapse, menu invoke, arm, reorder release, singleton Remove/MoveStep, and spacing Finish are inert before model/source/history/selection/primary/camera/reveal changes; include same-membership recreation and same owner/revision cases.
- Native/mounted first Shift press from text/Properties focus and after busy child replacement; release/repress of Shift; no focus theft from passive modifiers. Hit the same subject at message body/label, activation, lifecycle marker, Fragment header and Branch header; compare actual hit identity with selection and support/refusal.
- Noncontiguous C/A selection and pressed-member variants preserve semantic order and original primary on drag; stationary click collapses, selected-anchor Noop keeps redo, out/back origin remains threshold-crossed Noop, Escape cancels before clearing, and rejected member sets never move a subset.
- Saved default header/message singleton spacing still updates only coordinates; normal group drag refuses without changing any coordinate/order or primary. Armed saved semantic drag changes only SetSteps and preserves both affected/unrelated authored layouts; default spacing resumes afterward. Camera/visit/selection changes cancel both kinds of captured input as intended.
- Stale toolbar/index commands and context-menu Add/Remove/Inspect do not bypass envelope admission; property edits address only primary. Pending forms remain untouched on group refusal. Exact saved/derived surface changes clear group membership as specified.
- Use the current planner's complete model/source/history oracle for successful App operations, plus exact selection/primary and zero-history refusal/no-op checks. Reuse the existing spacing/controller and mounted passive-focus regression evidence; rerun affected workflows after changes. Source design alone establishes none of these runtime outcomes.