Graph Core Refactors & Architecture Doctrine
Doctrine: Graph as exposure layer, not universal store
The graph is an exposure/adaptor layer — it does not own the objects it exposes. Domain data lives wherever it naturally belongs: entity data in entity stores (entities/string/, entities/code/, etc.), activity-owned world surface state in activity sessions, LLM conversations in the LLM store (llm/), filesystem nodes on the real filesystem, kernel instances in the kernel system. The graph creates lightweight adapter nodes that project these domain objects into a uniform navigable topology.
Graph core persists only user-materialized topology — which visible node keys exist and which visible edge connections exist (graph:capture-state / graph:restore-state). Owning systems persist the actual object data. There is no "full graph state backup" that captures domain data; capture-state stores node keys and edge source/target keys only.
Related objects become graph-visible only through explicit preview, view, search-row, or action controls. A node may offer actions such as Show Code, Open Timeline, Show Run Steps, Reveal Failed Steps, Start, or a searchable picker that loads a selected key into the current graph map and adds any display edge needed for that user-visible relationship. Dense records such as logs, JSON, inputs, outputs, and errors belong in payload panels or full node views rather than generic detail toggles. Graph code should not rely on hidden relationship expansion hooks to bulk-materialize neighboring records.
Canonical terminology
| Use | Avoid |
|---|---|
| graph-exposed object, graph-visible object | graph-backed object, graph object |
| graph node adapter, projection | first-class graph object |
| entity-store-backed node, world-state-backed node, LLM-store-backed node | backed by the graph |
| graph topology state (node keys + edge keys) | full graph state, graph data, graph node data |
| exposed through the graph, visible in the graph | lives in the graph, stored in the graph |
| graph as universal interface / exposure layer | graph as universal model |
| domain object | graph-native object |
Key architectural facts
graph/core.fnl: nodes are lightweight records (key, label, color, view ref, graph ref). It exposes low-level primitives such asgraph:register-key-loader,create-node-by-key, and topology capture/restore.capture-statestores node keys and edge source/target keys only — no domain data.graph/extension-registry.fnl: the only node-type installation mechanism for built-ins and user/runtime extensions. Descriptors install owner-safe key-loader and morph handles into live and future world runtimes, then refresh map-local adapters by scheme during reload. See Reloadable Graph Extension Units.graph/extensions/builtins/: family-scoped built-in graph extension descriptors register through the app registry. Their installer functions call the low-levelgraph:register-key-loaderprimitive to adapt owning stores/systems into graph nodes on demand viaload-by-key; they do not own or persist domain records. Entity descriptors adapt entity stores; LLM descriptors adapt the LLM store; workflow descriptors adapt workflow stores; world activity and surface descriptors adaptworld-managerandWorldData.graph/map.fnl: graph maps hold the visible topology a user has materialized in that interaction context. Preview/search/action code loads keys through the activeGraphMapand inserts explicit display edges when the user asks to reveal related records.graph/outline.fnl: outline rows are aGraphMapprojection over graph-visible topology. The projection follows outgoing visible graph-map edges from the active map's orderedoutline_root_keys; it does not materialize hidden relationships or persist row state.- Graph presentation islands are map-local presentation state over graph-exposed objects.
GraphMappersists island records, whileGraphViewhosts presenters by kind. Presenter kinds such asordered-listmust not make domain stores subordinate to graph view; domain stores remain the source of truth for domain data. graph/world-data.fnl: activity-owned scene/HUD/canvas state is resolved fromworld.state.activity.sessions.<activity-id>throughWorldDatahelpers. Activity-owned graph keys include bothworld-idandactivity-id(for exampleactivity-scene:<world-id>:<activity-id>,activity-background:<world-id>:<activity-id>, andactivity-terrain:<world-id>:<activity-id>:<terrain-id>). Updates mutate the owning activity surface state, then sync to the active surface and persist world. Graph nodes are projections, not the source of truth.- Activity hierarchy keys expose
world:<world-id>→world-activities:<world-id>→world-activity:<world-id>:<activity-id>→activity-surfaces:<world-id>:<activity-id>before reaching concrete surface nodes such as scene, HUD, or canvas. graph/nodes/*.fnl: node constructors receive stores/world-manager, resolve domain records from them, and emit signals when underlying data changes.graph/view/: owns visual/interactive systems (spatial ForceLayout, points, labels, outline rendering, selection, movables, persistence metadata). Outline rendering is aGraphViewruntime concern selected from the activeGraphMap.view_mode; graph node adapters do not track view instances.
GraphMap outline projection and GraphView rendering
Outline view is an alternate runtime rendering of the active GraphMap. The map owns view_mode and ordered outline_root_keys as interaction context over graph-visible objects. The outline projection derives rows from graph-map topology; it is not persisted by graph core and is not tracked by graph node adapters.
GraphView chooses the spatial renderer or outline renderer at runtime and owns the resulting UI handles, hit targets, focus behavior, and row widgets. Graph core persists only graph topology: node keys and edge source/target keys. GraphMap:capture-state separately persists map-local interaction state such as view_mode and outline_root_keys.
Island member drag handles
Alt-dragging a presentation island member moves the island body on drag end. During the drag, only the dragged member follows the pointer; after drop, GraphView asks the island presenter to derive the next island state and then reconciles the island. Ordered-list islands store the moved body as island.state.position. Normal non-alt member drag remains snap-back under island reconciliation.
Preview vs UX node vs full view
Previews expose compact state, high-frequency local actions, and short search/list controls. They are appropriate for status summaries, small action rows, revealing one selected related node, opening a focused UX node, or opening a full view/panel.
In graph view, double-clicking a compact node point and pressing Enter on a focused graph node both expand the inline preview card. Enter expansion is idempotent: once the compact point has been replaced by an expanded card, another Enter leaves the preview expanded rather than collapsing it or opening the full view. Full views remain explicit actions through context-menu Open and expanded-card header controls.
UX-purpose graph nodes expose one focused operation/detail surface and own no domain records. They are graph-addressable adapters over owning stores or systems, such as workflow step explorers or run timelines, and they materialize related topology only through explicit user actions.
- Filesystem file content follows the same exposure-layer rule:
fs:<path>remains the generic path adapter, regular files expose explicit interaction rows, andfs-file-viewer:<absolute-path>is a UX-purpose node that reads only bounded windows. See Graph Filesystem File Interactions.
Full node views and panels handle dense content, long payloads, and editor-style interactions. Source code, logs, JSON, Fennel forms, multiline errors, inputs, and outputs belong in these views rather than in single-line preview labels.
Graph-selection actions must read active GraphMap selection, validate accepted node types, and fail loudly or display explicit graph-native status on invalid selection. Destructive actions must be explicit graph actions rather than confirmation-dialog flows.
Kind badges
Graph node adapters may expose kind-badge presentation metadata for preview-card and full node-view titlebars. Missing metadata derives compact badge text from a stable key scheme before the first : when one exists; explicit false opts out. Graph core persists only graph topology: node keys and edge source/target keys. GraphMap:capture-state separately persists map-local interaction state. Badge text and colors remain render-time presentation metadata and must not be written into graph topology state or map-local interaction state.
Compact labels
Graph node adapters may expose compact-label presentation metadata for collapsed graph labels. graph/view/labels treats compact-label: false as an explicit opt-out, a string value as the compact label text base, and missing metadata as the existing fallback to node.label or the node key. This metadata is render-time presentation state only. Graph core persists only graph topology, and GraphMap:capture-state separately persists map-local interaction state; neither persistence path writes compact-label metadata.
List entity adapters use this contract to keep raw ids available on non-compact surfaces while suppressing collapsed labels for unnamed lists. Named lists continue to expose their custom name as both the normal node label and compact label text base.
Extracted
- Graph model:
graph/coreowns nodes/edges, add/remove/replace, and emits signals (node-added,node-removed,node-replaced,edge-added,edge-removed). - Graph view:
graph/view(GraphView) owns ForceLayout, points, labels, selection, movables, persistence, and node dialogs. It listens to graph signals and can be dropped/recreated without touching the graph model. - Persistence:
graph/view/persistencehandles metadata load/save and scheduled writes. - Labels and LOD:
graph/view/labels(GraphViewLabels) manages label creation, LOD switching, camera-distance debounce, positioning, and cleanup. - Selection:
graph/view/selection(GraphViewSelection) owns selector wiring, selected-node bookkeeping, pruning, and signals. - Node view wiring:
graph/view/node-views(GraphViewNodeViews) builds/drops node dialogs or HUD embeds. - Movables:
graph/view/movables(GraphViewMovables) builds drag targets, registers/unregisters withapp.movables, forwards position updates, and schedules persistence on drag end. - Layout and edges:
graph/view/layout(GraphViewLayout) wrapsForceLayout, line updates, node positioning, and rebuilds. - View registry:
graph/view/registry(GraphViewRegistry) centralizes view-only bookkeeping (indices, node/point maps, edge map), replacement, and deduping. - Utils split:
graph/core/utilsholds core-only helpers (glm vec coercion).graph/view/utilsadds view helpers (label text truncation/wrapping) and re-exports the core glm helpers.
Label and LOD handling
- Core now forwards node→point maps to
graph/view/labelsto build spans and reposition them based on camera distance; tune LOD thresholds and label styling there instead of in core. - Benefits: keeps HUD text concerns and camera logic out of core; makes it easier to tune LOD thresholds separately.
Selection and node views
- Selection is owned by
graph/view+graph/view/selection; update behavior there when changing selector policies or selection logging. - Node dialogs are handled by
graph/view/node-views; tweak dialog builders or HUD embedding there without touching the graph model. - Graph nodes do not track view instances. Nodes expose a plain
:viewconstructor (e.g.:view HackerNewsStoryView) and emit state via signals (rows-changed,items-changed,targets-changed, etc.). Views subscribe to those signals (or call node methods likefetch,open-entry,add-target) and stay UI-only so multiple views can hang off the same node.
What stays in core
- Graph model: adding/removing/replacing visible nodes/edges and lifecycle (
drop). - No visual state: layout, labels, selection, movables, points, or persistence belong in
graph/view.
