2026 04 11_graph_tree_egui_tiles_decoupling_follow_on_plan - mark-ik/graphshell GitHub Wiki
Date: 2026-04-11
Status: ✅ Complete (2026-04-11)
Scope: Define the next migration stage after graph-tree crate extraction:
move from a parallel mirrored GraphTree to a genuinely authoritative
Workbench/Navigator tree model, retire per-frame egui_tiles mirroring, and
close the correctness gaps surfaced by the first extraction review.
Related:
-
2026-04-10_graph_tree_implementation_plan.md— original extraction and migration plan -
../../technical_architecture/graph_tree_spec.md— canonicalgraph-treecrate design -
workbench_frame_tile_interaction_spec.md— current Workbench authority and mutation semantics -
graphlet_projection_binding_spec.md— graphlet binding semantics consumed by GraphTree -
../navigator/NAVIGATOR.md— Navigator projection semantics to collapse into the shared tree -
../subsystem_ux_semantics/ux_tree_and_probe_spec.md— UxTree projection contract -
../../technical_architecture/ARCHITECTURAL_OVERVIEW.md— authority orientation
The graph-tree extraction has landed as a real workspace crate with:
- framework-agnostic core data structures,
- topology and navigation logic,
- layout computation,
- UxTree emission,
- unit and property tests,
- and initial Graphshell-side adapters and persistence hooks.
That is an important milestone, but it is not yet the same thing as GraphTree becoming the authority.
Current repository reality:
-
GraphTreeexists as a parallel model in the desktop shell, -
egui_tilesstill appears to be the live arrangement source, - and the app currently mirrors tile state back into
GraphTreeat startup and once per frame.
This means the extraction is real, but the semantic handoff is not complete.
The next stage is therefore not "extract the crate" but:
stop treating GraphTree as a disposable mirror and make it the semantic owner of workbench/navigator tree state.
The current integration shape is:
-
GraphTreeis restored from persistence if available. - The tile tree is then used to rebuild/synchronize the
GraphTree. - That synchronization happens again every frame.
This is visible in:
-
shell/desktop/ui/gui.rsstartup restore + rebuild path -
shell/desktop/ui/gui.rsper-framerebuild_from_tiles(...) shell/desktop/workbench/graph_tree_sync.rs
That is acceptable as a transitional bootstrap layer, but it creates two problems:
-
GraphTreecannot yet be trusted as the source of semantic truth, - and topology-preserving behavior can be flattened or overwritten by the tile mirror.
The initial extraction review surfaced four concrete follow-on concerns.
Traversal attaches can create members that exist in members but are not
reachable from any root if the source parent is missing or placement fails.
Root cause:
-
TreeTopology::attach_childrejects self-parenting and duplicates, but does not validate that the proposed parent is actually present in the topology. -
GraphTree::apply_attachinserts theMemberEntryeven if topology placement failed or implicitly targeted a nonexistent parent.
Consequence:
- member exists for persistence/counting/parity,
- but disappears from
visible_rows(), - and disappears from layout.
Required fix direction:
- either validate parent existence in
attach_child/reparent, - or make
apply_attachfall back to a safe root/anchor placement when the requested topology insertion is invalid.
This is a correctness bug, not merely a migration inconvenience.
The current rebuild_from_tiles path attaches previously unseen tile nodes with
Provenance::Restored, which the current GraphTree attach logic treats as a
root placement.
This is not a risk — it is the current behavior. The tile tree has no concept
of provenance: it does not know why a pane exists (traversal, manual add,
derived graphlet, etc.). Every node gets Provenance::Restored and becomes a
root. The entire traversal-derived parent/child topology is destroyed every
frame and rebuilt as a flat list of roots.
Consequence:
- traversal children and graph-derived placements are silently collapsed into roots on every frame,
- topology-preserving behavior cannot survive the migration phase under the current per-frame rebuild model,
- and any topology information set through
graph_tree_commandsis immediately overwritten on the next frame.
The current parity check only validates set-level membership and therefore will not catch this structural regression.
The crate contract is "one GraphTree per graph view," but persistence is
currently written through a single graph_tree_latest blob.
Consequence:
- multiple graph views cannot persist independently,
- future parallel trees or workspaces can overwrite each other,
- restored expansion/topology state can belong to the wrong view.
This must be re-keyed by GraphViewId before GraphTree can safely become the
authoritative persisted tree model.
Linked graphlet binding currently serializes anchor references through
format!("{:?}", node_key) — a debug-format string that produces output like
NodeIndex(42). This is a debug hack from the initial binding bridge, not a
stable identifier.
Fix: replace with NodeKey::index() as a stable integer. This is a 2-line
change in graph_tree_binding.rs (register_linked_graphlet) and should be
done in Phase A alongside other correctness hardening.
The memory_policy module produces SetLifecycle(Cold) actions for
origin-aware lifecycle demotion. During the transition period where
egui_tiles is still the live rendering owner, these actions must flow
through both systems: GraphTree processes the NavAction, and the host must
also update the tile tree to reflect the demotion.
This is the same dual-write problem that affects all command paths during transition (§6 Phase D), but memory policy is a new command source that did not exist when the tile tree was designed and has no existing tile-side equivalent. Phase D must account for it explicitly.
tile_compositor.rs (2,857 lines) keys content callbacks, GL state isolation,
and overlay passes by TileId. Phase E (layout from GraphTree) cannot land
without a compositor adapter that translates GraphTree::compute_layout()
results into the format the compositor expects. This is not a full compositor
redesign (§9), but it is a prerequisite adapter that Phase E must include.
The immediate goal of this follow-on is:
make GraphTree the semantic authority for topology, activation, expansion,
and layout intent, while shrinking egui_tiles into a temporary rendering host
or compatibility layer.
This does not require deleting egui_tiles on day one.
It does require stopping this current authority shape:
-
egui_tilesowns live truth -
GraphTreeis rebuilt from it repeatedly
and moving to:
-
GraphTreeowns semantic truth -
egui_tilesreflects or hosts that truth during transition
Reading an old egui_tiles layout and converting it into GraphTree is a
valid migration step.
Rebuilding GraphTree from egui_tiles every frame is not a valid long-term
authority model.
The semantic direction should become:
GraphTree -> projection/adapters -> host rendering/layout
not:
egui_tiles -> mirror -> GraphTree
except for explicit legacy import or compatibility-only repair paths.
During the parallel migration phase, parity diagnostics must compare:
- membership
- parent/child relationships
- active member
- expansion state
- visible member order
- visible pane set
Membership-only parity is too weak to protect the migration.
If GraphTree is "one per graph view," persistence must be keyed that way
before the tree becomes authoritative.
If egui_tiles remains temporarily, it should remain only as:
- a host for spatial pane rectangles,
- a compatibility presentation structure,
- or a thin adapter over
GraphTreelayout output.
It should not remain a second semantic owner.
Fix the problems that make authority migration unsafe.
Required work:
- reject or safely recover invalid traversal-parent placement
- ensure
reparentalso validates parent existence - add tests covering missing-parent attach behavior
- add tests that verify no member can exist in
memberswhile being unreachable from roots unless explicitly modeled as hidden/off-tree state - decide whether linked graphlet anchors need durable canonical identifiers now
Done gate:
- no orphaned invisible members can be created through attach/reparent actions
Retire the unconditional per-frame rebuild_from_tiles(...) path.
This is the highest-risk phase in the migration. Removing per-frame rebuild
means every mutation path that currently touches tiles_tree must also
dispatch the corresponding NavAction to GraphTree — or GraphTree drifts.
The coupling surface is large:
- ~40 functions in
tile_view_ops.rs(open, close, split, tab, focus, etc.) - compositor content callback registration
- persistence restore path
- webview lifecycle callbacks (map/unmap/crash)
- frame group operations
Transition strategy: dual-write. Every tile mutation site gains a
corresponding graph_tree_commands call that keeps GraphTree in sync.
This is the reverse of the current direction (tiles→GraphTree) but
preserves the same consistency guarantee until Phase D flips authority.
The dual-write layer should be a thin adapter — not 40 inline callsites —
so that it can be removed cleanly in Phase D.
Replace the per-frame rebuild with:
- startup import/bootstrap from tile state (one-shot migration)
- dual-write adapter for tile mutations during transition
- optional one-shot recovery or diagnostics repair path, not a frame path
Done gate:
- no frame path rebuilds
GraphTreefrom the tiles tree - dual-write adapter covers all tile mutation paths
Make all tree-shaped or tab-shaped projections resolve from GraphTree.
Includes:
- Navigator/sidebar member lists
- tree-style tab bars
- active/focused member queries
- focus cycling
- reveal/expand behavior
- graphlet membership decoration
Done gate:
- these surfaces can run correctly from
GraphTreewithout consultingegui_tilesfor semantic grouping truth
Route open/activate/dismiss/reparent/toggle-expand/reveal flows through the
graph_tree_commands layer first.
Any mutation of egui_tiles during transition should become a consequence of a
GraphTree change, not a sibling authority path.
Done gate:
- semantic workbench/nav commands are
GraphTree-first
Promote GraphTree::compute_layout() and the GraphTree layout facade to the
canonical pane-rect source.
This phase requires a compositor adapter. tile_compositor.rs (2,857 lines)
currently iterates tiles for content callback dispatch, GL state isolation,
and overlay passes, all keyed by TileId. The adapter must translate
GraphTree layout results (HashMap<NodeKey, Rect>) into the compositor's
expected input format — either by mapping NodeKey → TileId at the boundary,
or by rekeying the compositor from TileId to NodeKey.
At this stage, egui_tiles should no longer be the meaning-bearing layout
structure. It may still be:
- a temporary host widget,
- a rectangle presenter,
- or an internal adapter
but pane placement semantics should come from GraphTree.
Done gate:
- pane rects can be derived from
GraphTreealone - compositor can dispatch content callbacks using GraphTree-derived rects
Persist one GraphTree per GraphViewId.
Required outcomes:
- independent restore for multiple graph views
- no cross-view overwrites
- expansion/topology state scoped to the right view
- clear backward-compat migration from the current single-blob storage
Done gate:
- persistence shape matches the crate contract
Once semantic state, commands, layout intent, and persistence are all
GraphTree-owned, the project can decide:
- keep a minimal
egui_tilescompatibility adapter for desktop only, or - remove
egui_tilesentirely
This final step should happen only after the earlier authority shifts are done.
Done gate:
-
egui_tilesis no longer required as a semantic owner
Recommended next tasks in order:
-
Topology safety fix
- validate parent existence in topology operations or add safe fallback in
apply_attach - add unit/property tests for missing-parent traversal attaches
- validate parent existence in topology operations or add safe fallback in
-
Parity upgrade
- extend parity diagnostics beyond membership-set comparison
- compare topology, active member, expansion state, and visible ordering
-
Remove per-frame rebuild
- demote
rebuild_from_tilesto startup import + explicit repair tooling
- demote
-
GraphTree-first command routing
- ensure open/activate/dismiss/reveal/toggle-expand go through
graph_tree_commands
- ensure open/activate/dismiss/reveal/toggle-expand go through
-
GraphTree-first projection wiring
- navigator/sidebar/tree tabs/focus cycle read from
GraphTreeonly
- navigator/sidebar/tree tabs/focus cycle read from
-
Per-view persistence migration
- replace the single
graph_tree_latestblob with per-view storage keys
- replace the single
-
Graphlet anchor identifier decision
- either formalize stable anchor identifiers now or mark current string serialization as intentionally transitional
This follow-on is complete when Graphshell can truthfully say:
-
GraphTreeis not rebuilt fromegui_tilesevery frame, - no
GraphTreemember can silently disappear from visible topology due to an invalid attach, - traversal-derived parent/child structure survives the migration path,
- parity diagnostics can catch structural drift rather than only membership drift,
- persisted tree state is scoped per graph view,
- and
egui_tilesis no longer a peer semantic authority besideGraphTree.
This follow-on does not require:
- immediate deletion of all
egui_tilescode, - redesign of Workbench command semantics,
- redesign of Navigator click grammar,
- or replacement of the compositor pipeline.
It is an authority migration and correctness hardening pass, not a wholesale UI rewrite.