Repository navigation
fix: apply multi-child MovableList diffs as moves without losing elements - #1138
Merged
Merged
Conversation
…ents MovableListHandler::apply_delta deferred every delete and tracked moved slots with index arithmetic. When a delta moved two or more children (e.g. `delete 2, insert [b, a]`), the arithmetic deleted a moved child or placed elements out of order, and could underflow or call `mov` out of range behind an `unwrap()`. Undo/redo replay inverse diffs through the same handler, so they panicked with OutOfBound. apply_delta now plans which deleted children are re-inserted, deletes the rest right to left, then places each inserted value at a live cursor, locating moved children with get_child_index. Algorithm, invariant and pitfalls: context/movable-list-apply-diff.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
WASM Size Report
|
zxch3n
added a commit
that referenced
this pull request
Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 28, 2026
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
#1166) * test(loro.js): add a Rust differential suite for MovableList Drive the Rust WASM build and loro.js with the same random multi-peer edits, crossing engines on every sync, and require equal values, versions, per-element metadata and events. Profiles that diverge on the current MovableList model are recorded as known divergences. Design and findings: context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): compare event effects and replicate unordered reverts Event deltas that differ only in shape are checked by effect; reverts whose op order Rust leaves to hash order are checked by value and replicated from Rust; peers that both engines reject are retired; peer IDs are never reused; undo stays unchained; the minimizer keeps those rules. See context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow root Both follow Rust issues listed in context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust reference Load Rust through tests/support/rust-reference.ts like the other differential suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without a WASM build. Drop the separate workflow and scripts. insert/delete now converges on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing diff() A child created in the range whose keys were all set and deleted again is listed by Rust and omitted by loro.js, also on main and for any parent; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas The changeset named a test:differential script that does not exist. Rust's diff() can list a sequence with an empty delta where loro.js omits it (context/loro-js-movable-list.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
* test(loro.js): add a Rust differential suite for MovableList Drive the Rust WASM build and loro.js with the same random multi-peer edits, crossing engines on every sync, and require equal values, versions, per-element metadata and events. Profiles that diverge on the current MovableList model are recorded as known divergences. Design and findings: context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): compare event effects and replicate unordered reverts Event deltas that differ only in shape are checked by effect; reverts whose op order Rust leaves to hash order are checked by value and replicated from Rust; peers that both engines reject are retired; peer IDs are never reused; undo stays unchained; the minimizer keeps those rules. See context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow root Both follow Rust issues listed in context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust reference Load Rust through tests/support/rust-reference.ts like the other differential suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without a WASM build. Drop the separate workflow and scripts. insert/delete now converges on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): make import and importBatch atomic A failed import no longer leaves history, pending changes, version, containers, state or events half applied. In-place history changes are journaled and undone, state is rebuilt only when it had changed, events are emitted after success. Details: context/import-batch-atomicity.md ("loro.js"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing diff() A child created in the range whose keys were all set and deleted again is listed by Rust and omitted by loro.js, also on main and for any parent; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas The changeset named a test:differential script that does not exist. Rust's diff() can list a sequence with an empty delta where loro.js omits it (context/loro-js-movable-list.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): journal snapshot-sequence writes instead of copying per import Each import copied the whole snapshot-sequence table for rollback, so its cost grew with the number of snapshot-hydrated containers. Record each write in the import journal instead (context/import-batch-atomicity.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
…) (#1168) * test(loro.js): add a Rust differential suite for MovableList Drive the Rust WASM build and loro.js with the same random multi-peer edits, crossing engines on every sync, and require equal values, versions, per-element metadata and events. Profiles that diverge on the current MovableList model are recorded as known divergences. Design and findings: context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): compare event effects and replicate unordered reverts Event deltas that differ only in shape are checked by effect; reverts whose op order Rust leaves to hash order are checked by value and replicated from Rust; peers that both engines reject are retired; peer IDs are never reused; undo stays unchained; the minimizer keeps those rules. See context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow root Both follow Rust issues listed in context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust reference Load Rust through tests/support/rust-reference.ts like the other differential suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without a WASM build. Drop the separate workflow and scripts. insert/delete now converges on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): make import and importBatch atomic A failed import no longer leaves history, pending changes, version, containers, state or events half applied. In-place history changes are journaled and undone, state is rebuilt only when it had changed, events are emitted after success. Details: context/import-batch-atomicity.md ("loro.js"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): reject MovableList moves and sets of elements outside their history Port of #1125; see context/loro-js-movable-list.md (Validation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing diff() A child created in the range whose keys were all set and deleted again is listed by Rust and omitted by loro.js, also on main and for any parent; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas The changeset named a test:differential script that does not exist. Rust's diff() can list a sequence with an empty delta where loro.js omits it (context/loro-js-movable-list.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): journal snapshot-sequence writes instead of copying per import Each import copied the whole snapshot-sequence table for rollback, so its cost grew with the number of snapshot-hydrated containers. Record each write in the import journal instead (context/import-batch-atomicity.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two defects in
MovableListHandler::apply_delta(crates/loro-internal/src/handler/movable_list_apply_delta.rs). That function is the movable-list path ofLoroDoc::apply_diff,revert_to, andUndoManagerundo/redo. Both defects were found while cross-checking the loro.js port (#1131, review round 3, B3 and the mixed-operation random matrix).applyDiffthat moves several children drops one. Swapping twoLoroTextchildren with[{delete: 2}, {insert: ["🦜:" + b.id, "🦜:" + a.id]}]produced{l: ["A"]}instead of{l: ["B", "A"]}. Rotations, reversals, and children moved among scalars could also lose elements or put them in the wrong order.[text t2, "s"], two moves that cancel out, Rustdiff→apply_diff→undo(). Main hitsindex - 1withindex == 0: a subtract overflow in debug, and in release/WASMmov(0, usize::MAX)→OutOfBound→.unwrap()panic.B2 (adjacent
deleteitems dropping a freshly inserted value) was not reproducible in Rust. The publicListDiffItem→DeltaRopeconversion merges adjacent deletes. This PR adds Rust and WASM regression tests for that shape anyway, and the new algorithm does not rely on the merge.Root cause
The old code deferred every delete to the end. It tracked the slots vacated by moved children with ad-hoc index arithmetic (
deleted_indices,next_deleted,index_shift) and assumed a moved child's old slot lay behind the current index. Once two or more children moved, or a child was re-inserted before its own delete, the arithmetic went wrong in three ways:movbehindprocess_replacements(..).unwrap(), which is the undo panic.Fix
apply_deltanow runs in three passes. The invariant is documented in the module docs and incontext/movable-list-apply-diff.md.container_remapas before.live[cursor..]is always the unplaced claimed children of the current delete gap, followed by the unmoved originals. A retain skips both. A claimed child found before the cursor moves tocursor - 1. Claimed children are located withget_child_index(O(log n)).Behavior that is kept: container values without a claim create a new child and record the remap.
from_moveinserts whose child still exists elsewhere are skipped. The O(n)contains_containerscan is replaced by the state's child index. The old per-op position updates cost O(#deleted children) each; those are gone.Validation
crates/loro/tests/movable_list_apply_diff.rs: 9 tests.diff→apply_diff→ undo → redo (64 seeds × root/nested × 4 rounds).apply_diff, sync, and undo/redo: no panic, and the peers converge (48 seeds).cargo test -p loro -p loro-internal: 1188 passed, 0 failed.pnpm check(clippy--all-features -D warnings) passes.cargo fmtis clean for the changed files. The pre-existing formatting drift indag.rs,undo.rs,moon_transcode.rs, andshallow_checkout_equal_value.rsis deliberately not included.pnpm -C crates/loro-wasm build-dev: vitest 380 passed, including 2 newapplyDiffcases inmovable_list.test.ts. tsc, deno, bun, and the CommonJS smoke test pass.Performance
crates/loro/tests/perf_movable_list_apply_diff.rsis an ignored test (cargo test --release -p loro --test perf_movable_list_apply_diff -- --ignored --nocapture). Setup: native release, a 100k-element list, median of 5 samples, then the median across 3 alternating main/PR rounds.✗ means main produces a wrong list: rotate loses 1 child, reverse 1k loses 499, reverse 5k loses 2499. The reverse-5k undo rows are therefore not comparable, because main's undo restores a much smaller, already-broken change. A breakdown on this PR shows its undo is dominated by the checkout-based
diff()(103 ms at 8k children), which this PR does not touch.apply_diffof the inverse took 17 ms, about 2× issuing the same moves directly.Coordination with open PRs
50d200d8, movable-list op validation) touchesmovable_list_state.rs,loro.rs,state.rs, the import path, and adds lines toAGENTS.md/crates/loro-internal/AGENTS.md. fix: restore deleted mergeable children once on revert/apply_diff/undo #1134 (92e4780b, mergeable revert) toucheshandler.rs(the map path and itsuselines),loro.rs,state.rs, andundo.rs.handler/movable_list_apply_delta.rsand thestdimport line ofhandler.rs. Its AGENTS pointers sit in different hunks from fix: reject malformed movable list move/set ops on import instead of aborting #1125's.movable_list_invalid_ops,mergeable_revert, contracts, and the fullloro-internalsuite pass.KeptChangediffs.Follow-ups (not in this PR)
apply_diffinsert still panics inHandler::new_unattached(unreachable!()), for example"🦜:cid:0@1:Unknown(9)"from JS. This is pre-existing and shared withListHandler. It should become anErrvalidated before any op is applied.delete 3, insert [b, c, a]movesbandc. Deltas fromLoroDoc::diffonly move elements that really moved, so they are unaffected. An LIS pass could reduce ops for hand-written deltas.🤖 Generated with Claude Code