2026 03 08_graph_app_decomposition_plan - mark-ik/graphshell GitHub Wiki
Date: 2026-03-08
Status: ✅ Complete (2026-03-08 initial, Stage 5 cleanup through 2026-04 sessions) — all 4 extraction stages + 8 Stage 5 cleanup slices landed. active_selection mirror eliminated; canonical scope-based selection store is the sole authority.
Primary hotspot: graph_app.rs
Related:
../../../archive_docs/checkpoint_2026-03-27/graphshell_docs/technical_architecture/ARCHITECTURAL_CONCERNS.md2026-03-06_foundational_reset_implementation_plan.md2026-03-06_foundational_reset_graphbrowserapp_field_ownership_map.md
graph_app.rs had grown to 14,714 lines and mixed several incompatible responsibilities:
- reusable state/value types
- selection state and clipboard selection helpers
- bridge-carrier intent types and conversions
- reducer and app orchestration
- persistence/history/workspace/runtime coordination
That concentration makes it too easy for boundary changes to become "just edit the big file" work, which is the opposite of the foundational reset direction.
graph_app.rs remains the owning façade for GraphBrowserApp, GraphWorkspace, reducer logic, and authority-crossing methods, but supporting surfaces move behind dedicated child modules.
Initial intended layout:
graph_app.rs owner/orchestrator
app/selection.rs canonical selection model + undo snapshot carrier
app/intents.rs bridge-carrier enums + conversion helpers
app/history.rs history-only support types/helpers
app/workspace_commands.rs pending app-command queue support
app/workspace_routing.rs workspace/frame routing, picker, and membership support
app/workbench_commands.rs pending workbench-intent queue support
app/focus_selection.rs focused-view selection and transition support
app/persistence.rs persistence/open/save/autosave helpers
The file is not being "split for aesthetics"; each extracted module must represent a real boundary.
Cross-cutting guardrail:
- tracing/diagnostics/perf instrumentation should not regrow as ad hoc inline clusters across unrelated reducer methods; where the signal is durable, prefer thin helper surfaces or child-module ownership so the owner file stays readable as authority logic
Move low-risk, high-fanout support surfaces out first:
SelectionStateSelectionUpdateMode- clipboard selection request types
- undo snapshot carrier
-
WorkbenchIntent,AppCommand,ViewAction,GraphMutation,RuntimeEvent,GraphIntent - conversion helpers between the split intent categories
Done gate:
- call sites still import from
crate::app::* - no behavior change
-
graph_app.rsline count reduced materially
Status: Landed on 2026-03-08.
Implemented files:
app/selection.rsapp/intents.rs
Move history-only enums, summaries, and preview bookkeeping helpers behind a history-focused child module.
Candidate surface:
HistoryManagerTabHistoryCaptureStatusHistoryTraversalFailureReasonHistoryHealthSummary
Done gate:
- history vocabulary no longer sits beside unrelated app-command and reducer types
- history-specific tests can target the child module directly
Status: Landed on 2026-03-08.
Implemented file:
app/history.rs
Move open/save/autosave helpers and workspace layout persistence glue into a dedicated module owned by GraphBrowserApp.
Candidate surface:
- startup persistence open helpers
- session workspace layout save/load helpers
- autosave throttling/retention helpers
Done gate:
- persistence sequencing becomes locally auditable
-
graph_app.rsno longer carries most file I/O orchestration details inline
Status: Landed on 2026-03-08.
Implemented file:
app/persistence.rs
Split pending command queue helpers and frame/workspace routing glue from the reducer body.
Candidate surface:
-
pending_app_commandsdrain/enqueue/coalescing helpers - chooser/prompt request staging helpers
- frame-open routing support
- focused-view/focus-return staging helpers that are queue-adjacent rather than reducer-core logic
Done gate:
- app-command queue behavior is inspectable in one module
- manual
pending_*cleanup work becomes easier to replace with stricter queue semantics
Status: Landed on 2026-03-08.
Implemented file:
app/workspace_commands.rsapp/workspace_routing.rsapp/workbench_commands.rsapp/focus_selection.rs
After support surfaces are extracted, graph_app.rs should primarily hold:
GraphBrowserAppGraphWorkspace- reducer-owned mutation orchestration
- explicit authority crossings
Selection-specific correction required in this stage:
- remove or fully demote
workspace.selected_nodesas a compatibility mirror once remaining runtime call sites are migrated - keep canonical graph-selection ownership in the scoped selection store rather than in ad hoc runtime mirrors
- ensure undo/redo, focused-view helpers, and persistence snapshot carriers do not reintroduce dual-authority semantics
Exit target:
-
graph_app.rsunder ~9k lines without hiding logic in grab-bag modules - selection ownership is single-authority in runtime code, not canonical-plus-mirror
- no crate split
- no rename churn at public call sites
- no semantic change to the current bridge-carrier model during Stage 1
- no "misc.rs" dumping ground
Landed extraction slices:
-
app/selection.rsnow owns canonical selection state and undo snapshot carrier -
app/intents.rsnow owns the bridge-carrier intent family and conversions -
app/history.rsnow owns the history support vocabulary (HistoryManagerTab,HistoryCaptureStatus,HistoryTraversalFailureReason,HistoryHealthSummary) -
app/persistence.rsnow owns startup persistence open, workspace layout save/load, autosave retention/throttling, and persisted UI settings helpers via aninclude!-backed implementation shard -
app/workspace_commands.rsnow owns the pending app-command queue primitives, coalescing/sanitization helpers, snapshot/clipboard/data-dir command accessors, note/clip-open accessors, and tool-surface return-target staging helpers as an initial Stage 4 slice -
app/workspace_routing.rsnow owns unsaved-prompt request resolution, workspace/frame membership and recency helpers, frame-open routing, node-context staging, chooser flows, and pending node-open/import routing helpers as a second Stage 4 slice -
app/workbench_commands.rsnow owns the pending workbench-intent queue helpers (enqueue,extend,take, and test-only count inspection) -
app/focus_selection.rsnow owns focused-view selection mirroring, focused-selection readers, and focus-transition synchronization helpers - first Stage 5 cleanup slice landed: live selection mutation now routes through
app/focus_selection.rs, the UX tree reads focused selection rather than the compatibility mirror, and external test helpers prefer app selection APIs over directworkspace.selected_nodesaccess - second Stage 5 cleanup slice landed: undo/redo snapshots now capture canonical focused selection rather than the raw compatibility mirror, snapshot restore/reset flows route through focused-selection helpers, and graph-load/reset paths use centralized selection reset semantics
- third Stage 5 cleanup slice landed: internal
graph_app.rsbehavior tests now assert through focused-selection helpers rather than the compatibility mirror, leaving only the intentional stale-mirror regression as a direct test touchpoint - fourth Stage 5 cleanup slice landed: remaining runtime
graph_app.rsselection consumers now read through focused-selection helpers, including pin/unpin edge commands and hop-distance primary tracking, so direct compatibility-mirror access is gone from owner-file runtime code - fifth Stage 5 cleanup slice landed: shell-side omnibar ranking/signifier logic now also reads focused selection through helper APIs, so direct
selected_nodesfield access is confined toapp/focus_selection.rsand the intentional stale-mirror regression test - sixth Stage 5 cleanup slice landed: the workspace-level compatibility field was demoted and renamed to
active_selection, making its cache/projection role explicit while keeping canonical ownership inselected_nodes_by_view - seventh Stage 5 cleanup slice landed: the
active_selectioncache is now private to the app owner/focus-selection seam, so external code can only observe selection through helper APIs rather than by reading the cache field directly - eighth Stage 5 cleanup slice landed:
active_selectionwas removed fromGraphWorkspaceentirely and replaced by a canonical scope-based selection store, withSelectionScope::Unfocusedcovering no-view selection semantics without a separate runtime projection field - ninth Stage 5 extraction slice landed:
app/history_runtime.rsnow owns history preview gating, replay cursor reconstruction, failure recording, archive export/clear/curation operations, and the full timeline preview/replay reducer cluster, leaving the owner match with a single delegated history-runtime branch - tenth Stage 5 extraction slice landed:
app/graph_views.rsnow owns graph-view slot/layout persistence, graph-view registration and reconciliation, camera-target resolution, fit-lock handling, and queued camera/zoom command routing, removing another contiguous owner-file subsystem without changing reducer call sites - eleventh Stage 5 extraction slice landed:
app/runtime_lifecycle.rsnow owns renderer-node mapping, runtime block/crash state, active and warm lifecycle LRU policy, and the webview-created/url/history/scroll/title/crash reducer handlers, removing the remaining lifecycle and renderer-mapping cluster from the owner without changing public app APIs - twelfth Stage 5 extraction slice landed:
app/graph_mutations.rsnow owns graph add/remove/update helpers, edge mutation logging, traversal append logging, grouped-edge command expansion, node pinning, and destructive graph reset flows, removing the main graph-mutation orchestration cluster from the owner while preserving existing reducer entry points - thirteenth Stage 5 cleanup slice landed:
app/focus_selection.rsnow also owns hop-distance invalidation tied to primary selection, direct node selection entry, and scoped-selection pruning helpers used by graph-view reconciliation and graph mutation flows, whileapp/ux_navigation.rsnow owns tool-surface toggle transitions plus shared UX navigation transition emission so the remaining focus-surface diagnostics are not scattered across the owner - fourteenth Stage 5 cleanup slice landed:
app/startup_persistence.rsnow owns startup persistence open timeout handling, recovery/open diagnostics emission, and constructor-time graph recovery fallback so the owner constructor keeps the app-assembly path while the persistence-open instrumentation is isolated ahead of servoshell debt-clear work - fifteenth Stage 5 cleanup slice landed:
app/workspace_routing.rsnow also owns the reducer-side routed node-open acceptance helpers for frame/workspace opens, so frame/workspace resolution and the queued restore/open effects live behind one seam instead of splitting phase-2 host-open semantics between the owner reducer and the routing module -
graph_app.rsnow declares and re-exports those child modules from the owner façade, removing the prior duplicate/comment-wrapped legacy blocks while keeping the owner/orchestrator boundary intact
Measured result after landing:
-
graph_app.rs: 14,714 -> 8,526 lines
Remaining highest-value follow-on:
- remaining reducer clusters that still combine orchestration with dense event-specific side effects if another cohesive seam is desired after the runtime lifecycle, graph-mutation, and selection-boundary cleanups
- follow-on cleanup around the scope-based selection naming/documentation if further explicitness would help the servoshell debt-clear focus/input boundary work