fix(core): a children-list placement collision resolves by an intrinsic rank, not arrival order (C24) - #371
Open
vieiralucas wants to merge 10 commits into
Open
fix(core): a children-list placement collision resolves by an intrinsic rank, not arrival order (C24)#371vieiralucas wants to merge 10 commits into
vieiralucas wants to merge 10 commits into
Conversation
… not arrival order (C24) A position in a children sequence is keyed `(list, stamp)` and a document holds at most one, since `read_state` refuses a duplicate. Two ops can carry one stamp into one list and both be admissible — dedup is on `OpId` and the id-space record bounds only an honest mint — so the key is contended, and whichever arrived first took it. One op set folded to two states and two replicas encoded different bytes. `placement_index` now carries the owning node, and the smaller element id owns the key. A later-arriving winner evicts: the incumbent's placement goes, its move edge is withdrawn (`TreeMoves::remove`), and a node whose only placement was the one taken returns to `revealed_pending`. The slot is re-seated (`List::reseat`) rather than re-inserted, tombstones included, so a delete landing between the two ops does not freeze the loser's anchor into the dead run. `refold_moves` re-points `parents` for a deleted node too, an edge only eviction could reach.
…alone A birth's `(list, stamp)` key is where the born node's element id comes from, so a birth that lost it would leave a node whose id names a position it does not hold. Ranking by id alone let two forged moves strip a born node of both its placements; the emptied record then read as a reveal shell and put the node back on the movable side that the opposite order leaves permanently unplaced. A birth now outranks a move, which also carries the invariant the eviction path reads: a born node never loses its birth placement, so a record emptied by an eviction belongs to a node that never had a birth. Eviction also drops the `parents` edge the fold gave a shell, which was a byte difference between the two orders.
Only another birth can take a birth's key — a tagged and a tagless insert at one stamp derive different children and contend. Whichever loses is left materialised and unplaced, which is the state a move is admissible from; a losing birth that happened to arrive first held the key long enough for a move of its child to apply, so the order that refuses that birth has to let the same move apply too. Otherwise the move lands on one replica and waits on the other, and one op set folds two ways. A refused birth is now recorded as awaiting its first placement, and an eviction that empties a record restores that state for any node, not only a reveal shell. The `parents` edge still goes only where the fold wrote it. Spec grows an all-permutations oracle over the two births and a move of each child.
A contiguous delete welds its ids into one record, so evicting an id in the interior has to split it on both sides and let the re-delete weld it back. Every collision test so far contested an id at a run boundary, so the leading-split branch was unexercised — a mutation sweep of `List::take_dead` surfaced it as a surviving mutant. The new case builds a four-child run, deletes it whole, and contests the id one in from each end, then checks the snapshot re-encodes canonically.
Cold review found four more ways one op set folded two states, three of them reachable through this seam and one under it. The birth test read the registry, so an `XmlReveal` naming a derivable child id under the other kind made an honest birth answer "not born here" and handed its key to any smaller-id mover — the rank inverted by a forged shell. It is now pure in the key: a stamp derives exactly two children, and both are tried. A move can name exactly the child a birth at its key derives, and that is one node asking twice rather than two claimants. Ranked as a contest it left the move's edge in the log when the move arrived first and none when the birth did. An eviction dropped the loser's reachability edge by asking whether it was born *at this key* rather than whether the fold wrote the edge. It is now re-derived from the surviving move log, the way `refold_moves` derives a placed node's. A node's placement record is grown in arrival order and was encoded as stored, so two *honest* replicas that saw one node's two moves in different orders encoded different bytes. Nothing reads it positionally, so the encoding sorts it by key. And a decode restores "materialised, unplaced, and movable" from the parent link, so a reload does not forget that a losing birth stays movable — the guarantee the previous commit added was runtime-only.
… one Both halves of the split guarded their own insert on being non-empty, which reads as two chances to write a zero-length record and is what a mutation sweep kept pointing at. A record covers at least one id — the codec refuses a zero-length run — so that belongs at the one place a record is written.
A decode re-derives "materialised, unplaced, movable" from the parent link, which recovers a born loser. A shell has no such link — the one the fold gave it is correctly removed on eviction — so its movability does not survive a snapshot. Both orders agree either way; live and reloaded do not, which is C28's hole reached by a new route.
vieiralucas
force-pushed
the
core/c24-stamp-placement-collision
branch
from
July 29, 2026 00:38
f20f1fa to
549656d
Compare
… break The block read as a narration of two defects rather than of the rule. Same two reasons, in the present tense: the edge is recorded only once the key is held, and the pending mark is cleared only once a placement replaces it.
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.
Closes C24. A children-list position is keyed
(list, stamp)and a document holds at most one —read_staterefuses a duplicate. Two ops can carry one stamp into one list and both be admissible (dedup is onOpId; the id-space record bounds only an honest mint), so the key is contended and whichever arrived first took it: one op set, two states, two differentencode_statebyte strings.placement_indexcarries the owning node instead of being a set; the key goes to a birth over a move, then the smaller element id — a total order over data both replicas hold.Placement, its move edge (TreeMoves::remove, by the same undo-and-replay a late arrival uses), and — for a shell — itsparentsedge, with the shell returned torevealed_pendingso later moves of it are not refused forever.List::reseat) rather than re-inserted, sinceinsert_atis idempotent on the id. This reaches the tombstones: a delete landing between the two ops otherwise freezes the loser's anchor into the dead run on one replica and the winner's on the other.reseatlifts the id out of its run, re-seats it, re-deletes; a delete stays terminal.refold_movesre-pointsparentsfor a deleted node too — an edge only eviction could reach, and one that was a function of arrival order rather than of the move log.Spec:
crates/core/tests/xml_stamp_collision.rsfolds each shape in both orders and compares snapshot bytes — two moves, tagged vs tagless insert, birth vs move, a delete interleaved across four orders, a shell losing its only placement, and a born node two shells try to strip.ARCHITECTURE §Tree Moves + DECISIONS updated. Out of scope, filed on the board: C27 (the same collision one layer down, in a plain
ListInsert— no element id to rank by, reproduced) and C28 (the reveal-shell set is not persisted, so a restart refuses the move that places a shell — pre-dates this unit, reproduced).Summary by cubic
Fixes nondeterministic children-list placement collisions by ranking claims intrinsically (birth > move, then smaller element id) and making eviction, move-fold, and encode/decode order-free so replicas converge to identical bytes. Closes C24.
Bug Fixes
(list, stamp): birth outranks move; tie breaks by smaller element id; rank is pure in the key; a move naming the birth’s child is the same claimant.TreeMoves::remove, reseat viaList::reseat(tombstones included); if the record empties, mark the node as awaiting its first placement;parentsis re-derived from the surviving move log (deleted nodes included).parentsedges are dropped on eviction for byte-for-byte parity.Refactors
Written for commit 2c0ffae. Summary will update on new commits.