Morphs Architecture
This document describes the morph system used to transform one graph node/entity type into another while keeping references stable through identity nodes.
Goals
- Allow controlled type changes (for example
string-entity -> code-entity). - Keep stable references via
identity:*keys. - Keep morph logic store/domain-focused and decoupled from graph/view internals.
- Ensure open views (for example notebook/list item previews) refresh immediately after morph.
Current Scope
- Implemented source type:
string-entity - Implemented target type:
code-entity identity:*sources are intentionally non-morphable.
Key Modules
- Morph registry:
assets/lua/morphs/init.fnl - String->Code morph:
assets/lua/morphs/string-entity-to-code-entity.fnl - Morph UI:
assets/lua/morph-view.fnl - Morph launchable:
assets/lua/launchables/morphs.fnl - Graph morph adapter:
assets/lua/graph/core.fnl
Related entity/node modules:
- Code entity store:
assets/lua/entities/code.fnl - Code node:
assets/lua/graph/nodes/code-entity.fnl - Code node view:
assets/lua/graph/view/views/code-entity.fnl
Morph Registry Contract
Morphs.register(from-scheme, to-scheme, morph-fn, meta) registers a morph function.
Morphs.target-items(source-node) returns available targets for a single source node.
Morphs.apply(source-node, target, opts):
- Validates source/target.
- Rejects identity sources.
- Runs the registered morph function.
- Emits
morphs.morphedevent.
morphs.morphed payload
The event includes:
source-nodesource-keyfrom-schemeto-schemeresult(morph-specific structured result)
String -> Code Morph Behavior
Implemented in assets/lua/morphs/string-entity-to-code-entity.fnl.
Given string-entity:<id>:
- Reads source string entity.
- Creates code entity with:
name = ""language = "fnl"source = <string value>
- Updates all identities whose
target-keymatched old source key to new code key. - Deletes the source string entity.
- Returns result payload fields including:
source-keytarget-keytarget-typeupdated-identity-keys
Note: this morph function does not mutate graph state directly.
Graph Adapter (Event-Driven)
assets/lua/graph/core.fnl subscribes to morphs.morphed and performs graph-side reconciliation:
- Remove old source node (if present).
- Load/add target node by key.
- Emit
graph.node-morphedwithsource-key,target-key, and original payload.
This keeps graph updates outside morph functions.
Notebook/List View Refresh Behavior
Notebook/list nodes subscribe to both:
- identity store updates (
identity-updated/identity-deleted) - graph morph completion (
graph.node-morphed)
Why both:
- Identity update can happen before graph node replacement is fully visible.
node-morphedprovides a post-reconciliation refresh point.- This avoids stale type labels/previews in already-open notebook/list views.
Relevant files:
assets/lua/graph/nodes/notebook.fnlassets/lua/graph/nodes/list-entity.fnl
UI Behavior
Morph view (assets/lua/morph-view.fnl):
- Reads currently selected graph node (exactly one required).
- Shows morph target search list.
- Applies morph on selection.
- Displays status text for success/failure.
Testing
Primary tests:
assets/lua/tests/test-morphs.fnlassets/lua/tests/test-notebooks.fnlassets/lua/tests/test-list-entities.fnl
Coverage includes:
- target registration and listing
- string->code morph result
- identity target-key retargeting
- source deletion + target creation expectations
- notebook/list edge refresh on identity updates
- notebook/list refresh on
graph.node-morphed
Constraints and Decisions
- Identity nodes are non-morphable by design.
- Current language field is unconstrained string; default is
fnl. - If source is not wrapped in identity, morph replaces source entity only (no identity auto-creation during morph).
Next Work
- Add additional morph targets.
- Define shared schema for
resultpayloads across all morphs. - Optionally add dedicated metrics/logging around morph lifecycle events.
- Add e2e coverage for notebook/list visual update after morph.
