The CRDT is delete-wins (a tombstone is permanent), so undo cannot invert an
edit — it re-reduces the log without it. The materialized score is always
reduce(applied onto base); undo drops the last unit (a primitive, or a whole
transaction) so even a delete is undone — its tombstone is simply never
produced. One user action is one unit; a transaction's descriptor and members
undo together. redo re-appends the unit; a new edit forks history (clears the
redo stack). undo/redo/can_undo/can_redo on EditorSession; a shared
materialize/install path keeps the score, render, hit-test, and clefs in step.
Undo must not let ids be reused (a streamed op would equivocate; an entity id
would collide), so the session keeps a permanent append-only `authored` log
beside the active `applied` prefix. New op-id counters come from authored.len()
(monotonic), and event/pitch/transaction minting scans authored — so a unit's
ids stay reserved after it is undone or forked away. The causal context is
derived from the active head (active_prior_context + extend_context): it covers
the whole active prefix, compact while contiguous and dot-based after a fork, so
it never asserts coverage of a removed counter (which would strand a later op
pending). with_identity now guards on authored history, and applied_operations
is documented as the active prefix with authored_operations the full record.
authored history is a local high-water source, not streaming-consistent undo.
Tests: undo/redo a transpose; undo restores a deleted note; undo a split insert
and a make-room overwrite as one unit; undo an override-aware move (value +
spelling); a new edit forks history; a fork mints fresh op and entity ids
(not the undone ones); edits after a fork re-reduce cleanly; with_identity is
refused after an undone edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS