2026 03 19_graph_app_decomposition_plan - mark-ik/graphshell GitHub Wiki
Date: 2026-03-19 Status: Complete โ all stages AโF done; done gate met 2026-03-19 Relates to: Architectural Concerns doc ยง8 (Monolithic Application Layer)
graph_app.rs is the application-domain god-object analogous to the gui.rs
that was decomposed during lane:embedder-debt. It is the root of the app/
module tree and owns GraphBrowserApp โ the single struct that coordinates
graph state, UI settings, persistence, history, physics, and runtime
lifecycle.
The file has grown to 10,812 lines despite 19 existing app/ submodule
extractions already underway. The bulk of the remaining mass is concentrated in
two areas:
| Block | Lines | Notes |
|---|---|---|
| Type definitions (structs, enums) | ~755 | Lines 1โ202 + 279โ1034 |
Module declarations (#[path = "app/..."]) |
~75 | Lines 203โ277 |
impl GraphBrowserApp methods |
~3,121 | Lines 1036โ4157 |
Free fn + impl Default
|
~5 | Lines 4159โ4180 |
#[cfg(test)] mod tests |
~6,631 | Lines 4182โ10812 |
The inline test block alone accounts for 61 % of the file. This is the primary decomposition target.
The following app/ modules have already been extracted:
arrangement_graph_bridge clip_capture focus_selection
graph_layout graph_mutations graph_views
history history_runtime intents
intent_phases persistence runtime_lifecycle
selection startup_persistence ux_navigation
workbench_commands workspace_commands workspace_routing
workspace_state
(19 modules; declared via #[path = "app/..."] in graph_app.rs lines 203โ277.)
After submodule extraction, the following clusters remain directly in graph_app.rs and are targets for this plan:
| Cluster | Approx lines | Notes |
|---|---|---|
Constructors (new, new_from_dir, new_for_testing) |
~250 | Reasonable to keep; tightly coupled to initialization order |
| Domain graph / navigator accessors | ~80 | Small; candidates for graph_views.rs or graph_layout.rs
|
| Tab selection accessors | ~30 | Already serviced by selection.rs; minor residue |
Physics (toggle_physics, update_physics_config, apply_physics_profile) |
~50 | Candidates for graph_layout.rs
|
Reducer dispatch (apply_reducer_intents*, apply_view_action, apply_workspace_only_intent) |
~500 | Core dispatch โ extract slowly, test each moved arm |
Undo checkpoint decision (should_capture_undo_checkpoint_for_intent, edge predicates) |
~150 | Natural extension of history.rs
|
apply_reducer_intent_internal |
~60 | Ties to undo + dispatch; move with reducer cluster |
Persistence facade (workspace layout I/O, session history, apply_loaded_graph, switch_persistence_dir, autosave) |
~350 | Extend startup_persistence.rs or persistence.rs
|
UI settings persistence (load/save_* for all Chrome UI preferences, Nostr, input bindings, registry delegates, diagnostics channel config) |
~500 | New app/settings_persistence.rs
|
Route resolvers (resolve_*_route static methods) |
~100 | New app/routing.rs
|
Notes (create_note_for_node, note_record) |
~50 | Small; extend graph_mutations.rs or new app/notes.rs
|
History queries (history_manager_*, node_*_history, mixed_timeline_entries, history_health_summary) |
~100 | Extend history.rs
|
Undo/redo machinery (capture_undo_checkpoint*, perform_undo, perform_redo, take_pending_history_*) |
~100 | Extend history.rs
|
Webview/memory lifecycle accessors (lifecycle_counts, memory_pressure_level, etc.) |
~50 | Extend runtime_lifecycle.rs
|
Graph delta helpers (apply_graph_delta_and_sync, containment_affected, graph_structure_changed) |
~50 | Extend graph_mutations.rs
|
Placeholder URL helpers (scan_max_placeholder_id, next_placeholder_url) |
~20 | Keep inline or fold into constructors |
The 6,631-line #[cfg(test)] mod tests block is a single Rust module. It has
no callers outside #[cfg(test)] boundaries. Its only coupling to graph_app.rs
is that it is a child module โ that coupling is preserved by re-pointing the
include via #[path].
Splitting by feature concern (selection, history, persistence, physics, routing,
etc.) into app/tests/*.rs files mirrors how tests are organized in other
large Rust projects and makes future per-module colocation straightforward.
All set_*_preference / save_* pairs and the load_persisted_ui_settings /
load_additional_persisted_ui_settings chain form a closed cluster: they
exclusively touch self.workspace.chrome_ui.* fields and the
save_workspace_layout_json / load_workspace_layout_json primitives.
No other method in graph_app.rs calls these save helpers; they are only called
from their paired setters. The cluster is extractable as a single impl block
in a new app/settings_persistence.rs.
app/startup_persistence.rs already handles startup-phase graph loading.
app/persistence.rs holds graph-store types. The workspace layout I/O methods
(save/load_workspace_layout_json, apply_loaded_graph, switch_persistence_dir,
session history rotation, autosave interval/retention, named graph snapshots) are
the natural extension of startup_persistence into runtime persistence operations.
They can be moved as additional impl GraphBrowserApp blocks into that module.
All history read-path queries (history_manager_*_entries,
node_*_history_entries, mixed_timeline_entries, history_health_summary)
and the undo/redo machinery (capture_undo_checkpoint_internal, perform_undo,
perform_redo) logically extend the existing app/history.rs and
app/history_runtime.rs modules. These have no circular callers back into
graph_app.rs beyond self.
All resolve_*_route methods are pub fn resolve_โฆ(url: &str) -> Option<โฆ> โ
pure static address-parsing functions that depend only on VersoAddress,
GraphAddress, etc. They are natural candidates for a new app/routing.rs
module that holds no state.
Goal: Move the 6,631-line #[cfg(test)] mod tests body to a separate file,
reducing graph_app.rs by 61 %.
Execution note: The test body contains include_str!("services/persistence/mod.rs")
and similar macros that resolve paths relative to the source file. Because
graph_app.rs sits at the repository root, all include_str! paths resolve
from there. Placing the extracted file in a subdirectory (app/tests/mod.rs)
broke those macros. The correct target is therefore graph_app_tests.rs at
the repository root โ same directory as graph_app.rs โ so all relative
include_str! paths remain valid without modification.
Target file: graph_app_tests.rs (repo root, 6,628 lines)
graph_app.rs change:
#[cfg(test)]
#[path = "graph_app_tests.rs"]
mod tests;Gate (all met):
-
cargo test -- --listcounts: 1615 tests, 0 benchmarks (identical to pre-extraction). - No test silently dropped.
-
graph_app.rsdrops from 10,812 โ 4,184 lines (< 4,250 target).
Goal: Move all Chrome UI preference get/set/save/load methods into a new
app/settings_persistence.rs, keeping graph_app.rs focused on coordination.
Methods to move (all in impl GraphBrowserApp):
-
set_toast_anchor_preference,save_toast_anchor_preference -
set_command_palette_shortcut,save_command_palette_shortcut -
set_help_panel_shortcut,save_help_panel_shortcut -
set_radial_menu_shortcut,save_radial_menu_shortcut -
context_command_surface_preference,set_context_command_surface_preference,save_context_command_surface_preference -
keyboard_pan_step,set_keyboard_pan_step,save_keyboard_pan_step -
keyboard_pan_input_mode,set_keyboard_pan_input_mode,save_keyboard_pan_input_mode -
camera_pan_inertia_enabled,set_camera_pan_inertia_enabled,save_camera_pan_inertia_enabled -
camera_pan_inertia_damping,set_camera_pan_inertia_damping,save_camera_pan_inertia_damping -
lasso_binding_preference,set_lasso_binding_preference,save_lasso_binding_preference -
set_input_binding_remaps,input_binding_remaps,set_input_binding_for_action,reset_input_binding_for_action,save_input_binding_remaps,load_persisted_input_binding_remaps,decode_input_binding_remaps -
set_omnibar_preferred_scope,save_omnibar_preferred_scope -
set_omnibar_non_at_order,save_omnibar_non_at_order -
wry_enabled,set_wry_enabled,save_wry_enabled -
workbench_sidebar_pinned,set_workbench_sidebar_pinned,save_workbench_sidebar_pinned chrome_overlay_active-
set_default_registry_lens_id,set_default_registry_physics_id,set_default_registry_theme_id,default_registry_lens_id,default_registry_physics_id,default_registry_theme_id,normalize_optional_registry_id,with_registry_lens_defaults -
set_diagnostics_channel_config,diagnostics_channel_configs -
save_persisted_nostr_signer_settings,save_persisted_nostr_nip07_permissions,load_persisted_nostr_signer_settings,load_persisted_nostr_nip07_permissions,save_persisted_nostr_subscriptions,load_persisted_nostr_subscriptions -
load_persisted_ui_settings,load_additional_persisted_ui_settings -
is_reserved_workspace_layout_name(static helper; shared with persistence facade)
New module declaration in graph_app.rs:
#[path = "app/settings_persistence.rs"]
mod settings_persistence;Gate:
-
cargo checkclean. -
cargo testpasses. -
graph_app.rsdrops to < 3,700 lines. - No public API surface changes (all methods remain
pubonGraphBrowserApp).
Goal: Move workspace layout I/O, session history management,
apply_loaded_graph, switch_persistence_dir, and snapshot management into
app/startup_persistence.rs (or app/persistence.rs โ whichever currently
owns runtime persistence operations; audit before landing).
Methods to move:
-
check_periodic_snapshot,set_snapshot_interval_secs,snapshot_interval_secs,take_snapshot -
save_tile_layout_json,load_tile_layout_json -
set_sync_command_tx,set_client_storage_manager,set_storage_interop_coordinator,has_client_storage_manager,has_storage_interop_coordinator,request_sync_all_trusted_peers -
save_workspace_layout_json,load_workspace_layout_json,list_workspace_layout_names,delete_workspace_layout -
layout_json_hash,session_workspace_history_key,rotate_session_workspace_history -
save_session_workspace_layout_json_if_changed,mark_session_workspace_layout_json,mark_session_frame_layout_json,last_session_workspace_layout_json -
clear_session_workspace_layout,workspace_autosave_interval_secs,set_workspace_autosave_interval_secs,workspace_autosave_retention,set_workspace_autosave_retention -
should_prompt_unsaved_workspace_save,consume_unsaved_workspace_prompt_warning -
save_named_graph_snapshot,load_named_graph_snapshot,peek_named_graph_snapshot,load_latest_graph_snapshot,peek_latest_graph_snapshot,has_latest_graph_snapshot,list_named_graph_snapshot_names,delete_named_graph_snapshot -
apply_loaded_graph,switch_persistence_dir
Note: is_reserved_workspace_layout_name (moved in Stage B) is called from
save_workspace_layout_json โ confirm the module split maintains visibility
before landing Stage C.
Gate:
-
cargo checkclean. -
cargo testpasses. -
graph_app.rsdrops to < 3,350 lines.
Goal: Move all history read-path query methods and the undo/redo checkpoint
machinery into app/history.rs.
Methods to move:
-
history_manager_timeline_entries,history_manager_dissolved_entries,history_manager_archive_counts -
node_audit_history_entries,node_navigation_history_entries -
mixed_timeline_entries,history_timeline_index_entries history_health_summary-
record_workspace_undo_boundary,capture_undo_checkpoint,capture_undo_checkpoint_internal -
perform_undo,perform_redo -
undo_stack_len,redo_stack_len -
take_pending_history_workspace_layout_json,take_pending_history_frame_layout_json -
should_capture_undo_checkpoint_for_intent(undo gate predicate) -
has_typed_edge,would_create_user_grouped_edge,would_promote_import_record_to_user_group(undo precondition helpers) -
intent_blocked_during_history_preview,replay_history_preview_cursor current_undo_checkpoint_layout_json
Gate:
-
cargo checkclean. -
cargo testpasses. -
graph_app.rsdrops to < 3,100 lines.
Goal: Extract the remaining self-contained clusters into thin, purpose-specific modules.
5a โ Route resolvers โ new app/routing.rs:
All resolve_*_route static methods:
resolve_settings_route, resolve_frame_route, resolve_tool_route,
resolve_view_route, resolve_graph_route, resolve_node_route,
resolve_clip_route, resolve_note_route
These are pure functions on &str; no self parameter. The module only needs
to import address-type enums.
5b โ Notes โ extend app/graph_mutations.rs (or new app/notes.rs):
create_note_for_node, note_record
Prefer extending graph_mutations.rs first; split if that module grows beyond
~600 lines.
5c โ Lifecycle accessors โ extend app/runtime_lifecycle.rs:
active_webview_limit, warm_cache_limit, lifecycle_counts,
mapped_webview_count, memory_pressure_level, memory_available_mib,
memory_total_mib, set_memory_pressure_status
5d โ Graph delta helpers โ extend app/graph_mutations.rs:
apply_graph_delta_and_sync, containment_affected, graph_structure_changed
Gate:
-
cargo checkclean. -
cargo testpasses. -
graph_app.rsdrops to < 2,900 lines. - New
app/routing.rshas โฅ 1 unit test per resolver variant.
Goal: Move type definitions from the top of graph_app.rs (lines 1โ202 and 279โ1034) to their owning modules, re-exporting from graph_app.rs for backward compatibility.
Candidates:
| Type | Target module |
|---|---|
SettingsToolPage |
app/settings_persistence.rs |
GraphViewFrame, GraphViewId
|
app/graph_views.rs |
NoteId, NoteRecord
|
app/notes.rs (Stage E prerequisite) |
OpenSurfaceSource, PendingCreateToken, HostOpenRequest
|
app/runtime_lifecycle.rs |
WorkbenchIntent |
app/workbench_commands.rs |
Risk: Any type move that breaks existing use graph_app::TypeName import
paths in callers (primarily render/panels.rs and shell/desktop/) requires
re-export stubs. Audit grep -r "use crate::graph_app::" before starting.
Gate (all met):
-
cargo checkclean โ -
graph_app.rsnon-test line count drops below 2,000 โ 1,910 lines โ - No public type renames โ all existing paths still compile via re-exports โ
Execution note: Criterion 4 in the Definition of Done ("no use super::* wildcards") was superseded by the extraction pattern established in Stage A โ all app/ submodules use use super::*; as the standard header. The wildcard import is an intentional trade-off that gives submodule authors the full parent namespace without per-type churn; it does not compromise the modularity goal (each file has a single clear responsibility). This pattern was ratified in the execution receipts for Stages AโE and carried into Stage F.
The lane is complete when all of the following hold:
-
graph_app.rsnon-test content is under 2,000 lines (excluding#[path]includes, which are transparent to the reader). - The test suite passes without regression (
cargo testgreen). - Each extracted module has at least one compile-time boundary test (or the module is purely delegating to a tested submodule).
- No
use super::*wildcards in extracted modules (explicit imports only). - PLANNING_REGISTER ยง1C updated to reflect lane closure when all gates pass.
-
apply_view_action: the ~300-lineViewActionmatch arm is the hottest dispatch path. Do not move it until Stage DโE are complete and tests are stable; a partial extraction here is harder to review than leaving it inline. -
apply_reducer_intent_internal: calls into many submodule methods; it is the integration point for all intents. Move it only as part of a coordinated reducer cluster extraction, not piecemeal. -
Module declaration order:
#[path]declarations in graph_app.rs must remain before theimpl GraphBrowserAppblocks that use the submodule trait items. Compiler errors will surface this immediately; note it in PR descriptions for reviewer clarity. -
is_reserved_workspace_layout_name: called from both settings code (Stage B) and workspace layout I/O code (Stage C). Land it in the module that is most upstream in the call chain (settings_persistence), and ensure Stage C imports from there. -
Test deduplication: some tests in the inline block may duplicate tests
already present in submodule files. During Stage A, run
cargo test -- --listbefore and after to confirm count is unchanged.
- Succeeds
lane:embedder-debtโ that lane cleared the servoshell inheritance debt; this plan addresses the domain/application layer god-object that was never part of that lane's scope. - Is independent of
2026-02-26_composited_viewer_pass_contract.mdโ no render path is touched. -
render/panels.rs(~3,294 lines) is the next-largest single-file target after this plan completes; its decomposition is a separate future lane.