2026 03 27_lens_decomposition_and_view_policy_plan - mark-ik/graphshell GitHub Wiki
Date: 2026-03-27
Status: Implemented with follow-up opportunities; revised 2026-03-28 after SetViewLens removal and LensConfig replacement
Purpose: Refactor the Lens model from a monolithic per-view bundle into a compositional preset layer over explicit graph-view policy surfaces, then document the remaining follow-up opportunities after the persistence transition.
Related:
../system/register/lens_compositor_spec.mdlayout_behaviors_and_physics_spec.mdmulti_view_pane_spec.md2026-03-14_graph_relation_families.md../../research/2026-02-24_interaction_and_semantic_design_schemes.md../../research/2026-03-27_ambient_graph_visual_effects.md
The original codebase stored a broad LensConfig directly on GraphViewState
and replaced it wholesale when a view lens was applied or refreshed from the
registry.
That shape was useful to get runtime lens resolution, fallback diagnostics, and basic per-view behavior working. But it now creates three problems:
-
A Lens is overloaded: it is simultaneously acting as:
- a user-facing preset identity,
- a registry product,
- a view-state storage object,
- an override carrier,
- and a behavior contract.
- Constituent settings are not first-class: layout, physics, filters, overlays, and future family/emphasis policies cannot be independently owned, inspected, or overridden without mutating the whole lens bundle.
- Composition is weaker than the design intent: the current registry can resolve and shallow-compose lenses, but it does not provide the richer policy layering described in the research and architecture docs.
This plan resolved that mismatch by defining a clear split:
- Lens = named preset/composition surface
- View policies = first-class graph-view-owned settings that a lens may populate, override, or partially leave alone
Current as-built state:
-
GraphViewStatenow carries explicitlens_state,layout_policy,physics_policy,filter_policy,overlay_policy,presentation_policy, andrelation_policy. -
SetViewLenshas been removed. - Lens/layout/physics/filter actions now route through narrower graph-view intents and policy-aware mutators.
- Registry lens resolution now returns a narrower resolved preset shape instead of a monolithic runtime lens bundle.
-
LensConfigno longer exists as a stored runtime field; legacy snapshots load through a compatibility deserializer that hydrates the policy surfaces.
This plan does not remove lenses.
The UX research is correct that users should be able to adopt a named semantic mode such as "research", "containment", or "overview" without manually tuning every sub-setting. That remains valuable.
What changes is the authority model:
- a Lens should no longer be the sole storage authority for view behavior
- a Lens should become a preset source that resolves into explicit, inspectable view policies
The graph-view contract in multi_view_pane_spec.md already treats a
GraphViewId as the owner of scoped camera/lens/layout state. This plan
extends that idea and makes the constituent settings explicit rather than
implicitly packed into LensConfig.
Nothing in this plan changes the existing scope boundary:
- Graph owns graph truth.
-
GraphViewIdowns scoped view policy. - Lens application remains graph-view scoped.
- Workbench still hosts graph views without owning their semantics.
Today LensConfig mixes together:
- identity:
name,lens_id - physics:
physics - layout:
layout,layout_algorithm_id - presentation:
theme,overlay_descriptor - filtering:
filter_expr, legacyfilters
This creates a structural mismatch with the actual runtime:
- physics behavior is already partially separate in registry/settings surfaces
- layout algorithm selection already has an independent control path
- edge projection is already a separate graph-view policy
- filters already behave like a graph-view-local policy surface
- theme is mostly resolved globally rather than from the active lens
- lens refresh replaces the bundle instead of reconciling constituent policies
The result is that LensConfig is serving as a storage shortcut, not a
clean semantic boundary.
This has been implemented. GraphViewState now stores a stable view-policy set
instead of a monolithic runtime lens bundle.
Suggested model:
pub struct GraphViewState {
pub id: GraphViewId,
pub name: String,
pub camera: Camera,
pub lens_state: ViewLensState,
pub layout_policy: ViewLayoutPolicy,
pub physics_policy: ViewPhysicsPolicy,
pub filter_policy: ViewFilterPolicy,
pub overlay_policy: ViewOverlayPolicy,
pub presentation_policy: ViewPresentationPolicy,
pub relation_policy: ViewRelationPolicy,
// existing per-view/runtime fields...
}Suggested responsibilities:
pub struct ViewLensState {
pub base_lens_id: Option<LensId>,
pub applied_components: Vec<LensComponentId>,
pub progressive_source_lens_id: Option<LensId>,
pub display_name: String,
}
pub struct ViewLayoutPolicy {
pub mode: LayoutMode,
pub algorithm_id: String,
}
pub struct ViewPhysicsPolicy {
pub profile_id: Option<PhysicsProfileId>,
pub family_physics: Option<FamilyPhysicsPolicy>,
pub inline_profile: Option<PhysicsProfile>,
}
pub struct ViewFilterPolicy {
pub facet_expr: Option<FacetExpr>,
pub named_filters: Vec<FilterToken>,
}
pub struct ViewOverlayPolicy {
pub overlay_descriptor: Option<LensOverlayDescriptor>,
pub suppressed_effects: Vec<EffectId>,
}
pub struct ViewPresentationPolicy {
pub theme_id: Option<ThemeId>,
pub inline_theme: Option<ThemeData>,
}
pub struct ViewRelationPolicy {
pub edge_projection_override: Option<EdgeProjectionState>,
pub family_visibility: FamilyVisibilityPolicy,
}Notes:
- The implemented shape is narrower than the speculative sketch above; it uses
concrete policy structs already present in code rather than the full
Sourced<T>/inline_*design. -
theme_idhas not been promoted; the current implementation keeps presentation policy light and does not over-commit to per-view themed chrome.
A lens should become a sparse, compositional preset:
pub struct LensProfile {
pub id: LensId,
pub display_name: String,
pub layout: Option<ViewLayoutPreset>,
pub physics: Option<ViewPhysicsPreset>,
pub filter: Option<ViewFilterPreset>,
pub overlay: Option<ViewOverlayPreset>,
pub presentation: Option<ViewPresentationPreset>,
pub relation: Option<ViewRelationPreset>,
pub progressive_breakpoints: Option<Vec<ProgressiveLensBreakpoint>>,
}This remains the main future-facing registry refinement:
- a Lens is no longer the exact runtime storage shape
- a Lens becomes an input to policy resolution
To avoid accidental clobbering, each view policy should be able to distinguish:
- inherited-from-lens value
- explicit per-view override
- workspace default
- registry default/fallback
Suggested pattern:
pub enum PolicyValueSource {
RegistryDefault,
WorkspaceDefault,
LensPreset(LensId),
ViewOverride,
}
pub struct Sourced<T> {
pub value: T,
pub source: PolicyValueSource,
}This is now lightly implemented for the highest-conflict surfaces. Layout,
physics, filter, overlay, and presentation policy state can now record source
metadata such as RegistryDefault, LensPreset(..), ViewOverride, and
LegacySnapshot.
The current implementation intentionally uses explicit source fields on the
policy structs rather than a full generic Sourced<T> wrapper. That keeps the
runtime migration small while still enabling future diagnostics and reset UX.
This plan needs an explicit precedence rule so settings surfaces, diagnostics, and runtime reducers all agree.
For each constituent policy surface:
- explicit graph-view override
- active lens preset contribution
- workspace/user default for that policy surface
- registry fallback/default
Important consequence:
-
SetViewLensIdand per-view policy intents replace monolithic lens application, so unrelated explicit per-view policy overrides remain intact unless the user performs an explicit "reset this policy to lens" action.
Lens application should support two modes:
pub enum LensApplyMode {
ReplaceDerivedPolicies,
MergePreservingViewOverrides,
}Default behavior should be MergePreservingViewOverrides.
Use ReplaceDerivedPolicies only for explicit reset flows such as:
- "Reapply lens defaults"
- "Reset this view to lens"
Progressive lens switching should operate on the same policy model:
- switch the active lens preset contribution
- preserve explicit per-view overrides
- re-evaluate any policy surfaces that are lens-derived
This aligns with the existing progressive-lens design but avoids surprise replacement of local view choices.
This section defines the migration target for each current field.
| Current field | Target surface | Notes |
|---|---|---|
name |
ViewLensState.display_name |
Identity/display only |
lens_id |
ViewLensState.base_lens_id |
Preset identity, not storage authority |
physics |
ViewPhysicsPolicy.inline_profile |
Transitional only; prefer profile_id long term |
layout |
ViewLayoutPolicy.mode |
First-class view policy |
layout_algorithm_id |
ViewLayoutPolicy.algorithm_id |
First-class view policy |
theme |
ViewPresentationPolicy.inline_theme or remove |
Keep only if per-view theme is intentional |
filter_expr |
ViewFilterPolicy.facet_expr |
First-class view policy |
filters_legacy |
ViewFilterPolicy.named_filters or compatibility layer |
Migration-only |
overlay_descriptor |
ViewOverlayPolicy.overlay_descriptor |
First-class visual policy |
Additional fields that should stop pretending to be "outside" lens semantics:
- family emphasis /
FamilyPhysicsPolicy - per-lens ambient effect suppression
- family visibility presets for containment/traversal/arrangement overlays
Those belong in explicit policy surfaces and may be populated by a lens preset.
Current behavior has been replaced by narrower paths:
-
SetViewLensIdselects the active lens identity - layout and physics have dedicated per-view intents
- filter state has dedicated per-view intents
Implemented behavior:
- resolve
LensProfile - update
view.lens_state - apply preset contributions into policy surfaces using precedence rules
- preserve explicit per-view overrides
- emit diagnostics indicating which policy surfaces changed
Implemented refresh behavior is a targeted recomposition pass:
- recompute only policy surfaces sourced from the affected lens
- leave
ViewOverridevalues intact - preserve unknown future policy surfaces
Graph-scoped controls no longer edit a cloned LensConfig blob.
Instead:
- the Lens chip chooses the active preset
- the Physics chip edits
ViewPhysicsPolicy - the Layout chip edits
ViewLayoutPolicy - filter chips edit
ViewFilterPolicy - future family/layer chips edit
ViewRelationPolicy
This makes the UI match the actual authority model.
Diagnostics should report both:
- active lens identity
- resolved constituent policy state with provenance
That gives real observability into "what the lens contributed" vs "what the view overrode".
After this refactor, lenses become more useful, not less.
- fast semantic mode switching
- shareable presets
- workflow defaults
- progressive zoom-stage transitions
- mod-authored bundles
- named visual/semantic grammar for a view
- selecting a different layout algorithm
- changing only the physics profile
- applying or clearing a facet filter
- toggling relation-family emphasis
- suppressing a visual ambient effect that clashes with the current lens
These should become direct policy edits.
Completed.
- Added policy structs to
GraphViewState. - Updated diagnostics/tests to observe the new policy structs.
Completed.
- Replaced wholesale lens replacement with policy-aware application logic.
- Removed
SetViewLens. - Preserved explicit per-view overrides by default.
- Changed registry refresh to operate through resolved preset recomposition.
Mostly completed.
- Lens picker edits
ViewLensStateviaSetViewLensId. - Physics/layout/filter controls edit their dedicated policy surfaces.
- Remaining follow-up is UX polish for explicit "reset this policy to lens" affordances.
Partially completed.
- Registry lens resolution now returns a narrower resolved preset shape.
- The registry still conceptually produces full lens presets;
LensProfileas a first-class sparse registry product is still future work. - Theme/physics IDs remain a mix of dedicated IDs and inline values depending on surface.
Completed for runtime state; compatibility remains only at deserialization edges.
- Removed direct runtime storage dependence on monolithic
LensConfig. - Retained deserialization upgrade logic for legacy snapshots.
-
filters_legacystill exists as a compatibility concern and can be retired after the migration window closes.
- Decide whether lenses become a first-class sparse
LensProfileregistry contract or remain represented by the current resolved preset shape. - Surface the new provenance metadata in diagnostics and graph-view chrome.
- Add explicit reset affordances that consume that provenance cleanly.
- Retire
filters_legacyafter the migration window closes.
This plan adds more structs and provenance handling. That is real complexity. But it replaces hidden complexity that already exists in reducer/UI/runtime behavior.
If product direction keeps theme global, do not over-engineer per-view theme. In that case:
- remove theme from lens runtime authority
- keep only a decorative overlay/presentation role for lenses
- leave theme as workflow/global chrome state
Once lenses become sparse preset bundles, authoring and diagnostics need to make it obvious which policy surfaces a lens actually controls.
That means:
-
describe_lens(id)should list affected policy surfaces - diagnostics should show resolved contributions and override sources
Current acceptance status:
- Done: applying a lens no longer overwrites unrelated explicit per-view settings by default.
- Done: registry-backed lens refresh preserves explicit graph-view overrides.
- Done: layout, physics, filter, and overlay policies are individually
inspectable in
GraphViewState. - Done: graph-scoped chrome controls edit constituent policies directly rather than mutating a cloned monolithic lens bundle.
- Pending/future: progressive lens switching semantics beyond the current lens identity model.
- Done: legacy snapshots containing
LensConfigstill load through a deterministic upgrade path.
This decomposition should be treated as the baseline architecture.
The current code now behaves like a decomposed policy-first system with a small legacy deserialization shim. Making the constituent policy surfaces explicit has aligned:
- the runtime with the docs,
- the UI with the authority model,
- and the lens concept with its intended product role.
The key principle is simple:
A Lens should be a named preset over graph-view policy surfaces, not the sole storage container for those surfaces.