7.6 KiB
Runtime deltas: design context and follow-up plan
Working notes for the in-vivo runtime delta effort on this branch, recording what exists, the motivation behind each decision, and the context needed to complete the remaining phases. The decisions here were settled in design discussion between Keavon and TrueDoctor during July 2026.
What exists on this branch
graph_craft::runtime_delta::RuntimeDelta: the structural delta enum. Payload-carrying, constructed at mutation time, plain runtime types, no serialization. Variants:AddNode,ReplaceNode(carryingBox<DocumentNode>),RemoveNode(address-only),SetInput,SetExport,SetVisibility.editor::...::network_interface::editor_delta: the editor-side wrapperEditorDelta { Graph(RuntimeDelta), NodeMetadata { .. } }andconstruct_batch, which turns one gesture's deltas into storageRegistryDeltaops.- Parity tests (
editor_delta_tests) proving the constructed ops equal the ops a whole-document diff would stage: exactly (op-for-op) for slot, export, visibility, and metadata changes; by stored-state equivalence for structural add/remove batches. - Substrate reused from the earlier marker-based experiment:
ScopedConversion(per-entity conversion through the canonicalfrom_runtimeencoders),PathResolver(path-hash ID derivation without conversion),Session::stage_computed_ops,GddV1::stage_runtime_deltas.
Not yet wired: mutation sites do not emit deltas, staging does not consume them, and the compiler still receives whole-network clones.
Decisions and their motivations
In-vivo payload deltas, not dirty-set markers. An earlier experiment (branches
runtime-deltas-1-record / runtime-deltas-3-flip, kept for reference) recorded scope markers and
re-derived deltas at staging by scoped reconversion. Rejected because the perf case did not hold
holistically: the editor stages after every mutation for the compiler anyway, so the data is in
hand at mutation time regardless, and payload capture costs nothing extra. Payloads also make the
delta a bidirectional currency (storage, compiler, and later undo/collab all speak it), which
markers structurally cannot.
Graph-craft structural enum plus an editor wrapper. The compiler must consume deltas without
editor dependencies, and a separate compiler-only delta type (a third set) was explicitly ruled
out. SetInput/SetExport are separate variants so the editor's InputConnector type does not
leak into graph-craft.
Wholesale metadata copies, diffed by storage. Rather than per-field metadata variants, one
NodeMetadata delta carries a copy of the node's persistent metadata (cheap to clone). Storage
diffs it against the working registry so minimal attribute ops fall out; the compiler throws it
away. This also absorbs ambiguity: the delta does not need to know which fields changed.
SetVisibility is structural. visible is a DocumentNode field the compiler consumes for
rendering. The rare other scalar setters (call_argument, context_features) go through
ReplaceNode instead of dedicated variants.
Batch-scoped construction. Removal closures and resource liveness are properties of the whole
gesture, not one delta: deleting a group with children removes several nodes whose shared
declaration only becomes unreferenced once all of them are gone. construct_batch is therefore
the construction unit, matching how mutations emit (compound operations produce several deltas)
and how staging consumes.
Arc-backed input values (agreed, not yet implemented). NodeInput::Value's TaggedValue
storage should become Arc-backed (inside the existing MemoHash), mutating via Arc::make_mut.
Delta capture, compiler updates, and undo snapshots then share by pointer bump instead of cloning;
copy-on-write fires only when an old version is genuinely still held, which is exactly when a copy
is semantically required. This is a prerequisite for cheap drag/paint deltas and should land in
graph-craft before the mutation sites are wired.
Accumulation and desync carry over from the earlier design. A pending buffer on the interface
accumulates delta batches with same-target coalescing (a drag keeps first-to-last one SetInput).
Anything that cannot itemize its changes (document open, snapshot install, raw network access,
storage mount) desyncs the buffer, forcing one whole-document diff before delta staging resumes.
The verify_journal_projection soak pattern from the earlier branches (constructed ops compared
against the whole-document diff on every staging under the validate_storage_round_trip
preference) is the validation harness to reuse; it caught real bugs twice there.
Remaining phases to land forward deltas
- Arc-backed values in graph-craft.
- The pending buffer on
NodeNetworkInterface(accumulation, coalescing, desync states). - Site wiring: roughly thirty mutation sites emit their delta batches, capturing state after
mutating. The site catalog from the earlier experiment maps every
transaction_modifiedcall site to what it touches. - Staging:
construct_batchintoSession::stage_computed_opsviaGddV1::stage_runtime_deltas, with the whole-document diff as the desync fallback and the parity soak in validate mode. - Compiler consumption: replace the per-update whole-network clone in
GraphUpdatewith theRuntimeDeltastream reconciling a mirror on the runtime thread, using apply-reported change as the invalidation signal.
Follow-up PR: storage-driven undo (decided: debug-compare rollout)
Direction: undo/redo stop installing interface snapshots and instead replay deltas returned by the storage layer, whose retirement machinery already precomputes every delta's reverse and applies it to the working registry on cursor moves.
Shape agreed in discussion:
Session::undo/redoreturn theRegistryDeltaops they applied (today they are applied and discarded).- A backward projection turns those ops into
EditorDeltas: slot and export ops via single-inputto_runtime;AddNode/AddNetworkops materialize nodes from the op payloads (reverses carry full snapshots, so nothing is missing); attribute ops collapse into oneNodeMetadatadelta per touched node, read from the post-move registry through the existingto_runtimemetadata path; global-to-local IDs resolve via the stashedoriginal_node_idattributes. NodeNetworkInterface::apply_deltas(&[EditorDelta])maps each variant onto existing mutation primitives plus a shared cache-invalidation epilogue. Applying must not re-record into the pending buffer (the ops are already history), so apply runs with recording suppressed; that suppression flag is the one new piece of interface state.
Rollout decision: debug-compare. Legacy snapshot undo remains authoritative while the storage-driven path runs in parallel, applying to the same interface state and comparing against the snapshot result; divergence logs in release and panics in tests, mirroring the existing round-trip soak conventions. Snapshots are deleted only once the compare has been quiet across the test suite and real use.
Known open points for that PR:
- Compare granularity: full interface equality per undo step is the strongest check and is likely affordable at undo frequency; decide whether metadata-only divergence should fail equally hard.
- Selection and view state are not in the registry, so storage-driven undo must leave them to the existing selection-history machinery rather than expecting them back from deltas.
- The same backward projection and apply pair is the future collab receive path; keep signatures free of undo-specific assumptions.