Files
ARCADIA-Studio/harness/experiments/diagram-removal-windows-20260912/state-action-delete-design-20260912.md
T

9.0 KiB
Raw Blame History

Persisted state row removal design — 2026-09-12

Read-only review at root dc1506f2e8fd2fb4e92efaba5b1d9425a6129e47. No production edits, compiler/Cargo, native application, Gitea or WSL operations. Parent requested this bounded extension while its full suite runs and confirmed that StateAction and internal StateTransition rows are both in diagram-removal scope.

Decision

Use the existing captured semantic confirmation for both typed StateAction rows and internal StateTransition rows. A row represents a maintained relation with its own ID, not a placement or its referenced work. Delete and Shift+Delete should confirm deleting that relation. No new placement semantics, schema, command or confirmation lifetime is needed.

The existing canvas already selects exact relation IDs:

  • behavior/state_canvas.rs:362 builds typed action rows from StateAction relations whose source is the displayed stable State/Mode. It sorts Entry, Do, Exit, then relation ID. Collapsed nodes have no rows.
  • state_canvas.rs:328 builds internal-transition rows from internal StateTransition relations whose source is that stable state. Collapsed nodes have no rows.
  • state_canvas.rs:543 checks action/internal row hit rectangles before the containing node body and returns the relation ID. Left click at lines 891–892 emits Event::Select(context, relation_id); right click at lines 842–854 captures that same ID with placement: None.
  • behavior.rs:1076 resolves both relation kinds back to the owning state machine through the relation source. App::select/refresh_draft retain the relation selection and derive no placement for it.
  • Rendering at state_canvas.rs:1470 highlights the exact selected StateAction relation row. Text comes from its phase and referenced work, not a fabricated child occurrence.

Both gates presently reject these rows: state_canvas.rs:803 only admits placements or view.edges, and app/diagram_removal.rs:77 only admits projected edges/ports when no placement is present. Core projection.rs:397–401 intentionally includes only non-Internal StateTransition edges in a dedicated state view; StateActions and Internal transitions therefore are not missing routed edges to add to the core projection.

Minimal shared visibility contract

Add a small pub(crate) fn visible_row_relation(project: &Project, diagram: Id, view: &ProjectedView, id: Id) -> bool beside the existing row helpers in behavior/state_canvas.rs. All three callers below should use this function. It must not admit a relation merely because project.contains(id).

  1. Resolve project.diagrams[diagram].projection.state_machine() safely; return false unless this is a dedicated state-machine diagram.
  2. Resolve the exact relation ID and require the source to belong to this machine using projection::state_machine_contains. For an Internal StateTransition, require its target to belong too. Only accept StateAction or StateTransition with transition_kind == Internal.
  3. Iterate the supplied current projected nodes, finding a node for that relation's source. Reuse state_action_rows(project, node) or internal_rows(project, node) and require an exact matching relation ID among those rows. This reuses the renderer's stable-state and !collapsed checks rather than duplicating its layout eligibility.

This covers collapsed parents: core projection.rs:298–321 omits placements hidden behind a collapsed visual ancestor, so no matching source node exists. A collapsed source node remains projected but the row helper returns empty. Explicit membership prevents another state machine's relation from becoming eligible through a stale selection or inappropriate supplied view. The helper's diagram and view must both come from the current captured context; callers already have them.

Three production call sites:

  • In the existing Delete branch of state_canvas.rs, extend the no-placement admission predicate with visible_row_relation(self.project, self.diagram, &scene.view, object). Keep its current editable, Select-tool, press/pan/source and exact modifier guards. Emit the existing Intent with object = relation_id, placement = None; the model flag may remain modifiers.shift() because no-placement intents already take the confirmation branch for either key.
  • In app/diagram_removal.rs, extend the no-placement visibility predicate only when self.page == Page::Behavior, using the same helper and intent.context.diagram. Keep all current context, selection, editor, modal, connection, bend and placement validation. Existing begin_model_delete_target(intent.object, Some(context)) captures the correct relation ID.
  • In behavior/state_menu.rs:187, use the same helper alongside view.edges when deciding the Delete / Shift+Delete hint. No Hide command or placement shortcut should appear for rows.

No new modifier or gesture route is necessary. The actual Iced focus owner admits keyboard events; the existing state Delete branch rejects active move/create/connect/reconnect/pan gestures. App again validates current scope and clean editors. Confirmation snapshots/token lifetime remain unchanged. Do not replace the typed relation ID with relation.target(); that would delete reusable Activity/Function work.

oa-core/src/session.rs:638 routes Command::Delete through the established cascade. references.rs:275 starts impact from the requested ID and expands ownership/dependents, not a relation's source/target endpoints. Deleting a StateAction link therefore preserves both its State/Mode and referenced Activity/Function; another state's action relation targeting that same work survives. Any legitimate dependents of the deleted relation still use the existing command and Undo rules.

One realistic Mounted test function

Add to crates/oa-desktop/src/app/diagram_removal_tests.rs, reusing its Mounted, delete/key helpers and source consistency checker. Start from mounted_state_delete_uses_state_placement_and_preserves_architecture_placement, not the standalone private-State canvas tests. Existing geometry/identity evidence is in state_canvas.rs:2122 (typed_state_actions_are_named_selectable_rows_before_internal_transitions_and_regions).

Suggested single test function loops four fresh cases: Entry, Do, Exit StateAction and Internal StateTransition. A compact SA fixture is sufficient for this identity-specific regression; OA uses the identical shared State/Mode row machinery.

  • Create a machine, region, two states, and one reusable Function. Create three StateAction relations (Entry/Do/Exit) from the first state to work, with distinct names; create another action relation from the second state to the same work. Include one internal transition on the first state. Empty legacy entry/do/exit text keeps geometry explicit. Persist the state view via state_edit::create_view, then mount App on Behavior with its active owner and saved state diagram.
  • Set zoom/pan deterministically and settle. Get the actual canvas bounds from the diagram-keyboard-owner focusable geometry and source state's current projected position. Typed rows start at state.position.y + 64 with 28-pixel spacing and 26-pixel hit height for this fixture. The internal row starts after the three action rows, at state.position.y + 64 + 3*28, with height 52. Translate the chosen row center by App state zoom/pan and actual canvas origin. This uses real pointer selection and no private canvas state seeding.
  • Click that center and assert selected == exact relation ID, state placement == None, and selected Function/State ID has not replaced the relation selection.
  • Snapshot the source bundle. Focused Delete must emit StateDiagram(Remove(..)) and open confirmation targeting this exact relation with no placement. It must leave the model and source untouched before confirmation. Cancel, click the row again to reacquire focus, and Shift+Delete should target that same relation. Confirm using the actual captured token.
  • Assert only the targeted relation is removed in this fixture; the Function, both States, their placements, other phases, and the second state's relation referencing the same Function survive. Check source/model consistency. Undo must restore the exact source bundle and every relation in one unit.

For the new helper's own correctness, small local assertions using the actual projected views should cover source collapsed, ancestor collapsed, wrong machine/diagram and unrelated relation ID; avoid broad new duplicated App guard tests. Existing context/token/editor/modifier/gesture guards are unchanged.

The currently generic confirmation names the relation (project.named(captured_id)), which is the correct deletion identity. If row text and relation names are routinely ambiguous, a narrow display improvement can additionally describe phase + referenced work + source state using the captured relation. This is optional clarity work, not a reason to redesign confirmation or change its target.

This extension closes two concrete persisted-state diagram selection gaps. Explorer shortcuts are a separate surface and are not asserted here to be required for issue 15.