Skip to content

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
mainfrom
core/c24-stamp-placement-collision
Open

fix(core): a children-list placement collision resolves by an intrinsic rank, not arrival order (C24)#371
vieiralucas wants to merge 10 commits into
mainfrom
core/c24-stamp-placement-collision

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Jul 28, 2026

Copy link
Copy Markdown
Member

Closes C24. A children-list position is keyed (list, stamp) and a document holds at most one — read_state refuses a duplicate. Two ops can carry one stamp into one list and both be admissible (dedup is on OpId; 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 different encode_state byte strings.

  • placement_index carries 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.
  • A birth outranks because the key is where the born node's id comes from. It also carries the invariant eviction 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 (a reveal shell).
  • A later-arriving winner evicts: the incumbent's Placement, its move edge (TreeMoves::remove, by the same undo-and-replay a late arrival uses), and — for a shell — its parents edge, with the shell returned to revealed_pending so later moves of it are not refused forever.
  • The slot is re-seated (List::reseat) rather than re-inserted, since insert_at is 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. reseat lifts the id out of its run, re-seats it, re-deletes; a delete stays terminal.
  • refold_moves re-points parents for 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.rs folds 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

    • Deterministic owner for (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.
    • Consistent eviction: drop the incumbent’s placement, withdraw its move via TreeMoves::remove, reseat via List::reseat (tombstones included); if the record empties, mark the node as awaiting its first placement; parents is re-derived from the surviving move log (deleted nodes included).
    • Order-free record: placements encode sorted by key; decode restores “materialised, unplaced, movable” so a losing birth stays movable; shell parents edges are dropped on eviction for byte-for-byte parity.
    • Tests cover move/move, tagged vs tagless insert, birth vs move, deletes interleaved, shell and born-node edge cases, all permutations of two births plus a move of each child, and interior dead-run evictions with canonical re-encode.
  • Refactors

    • Split-run handling writes no empty records. Spec/docs name the rank and collision rule and clarify the move gate: the edge is logged only once the key is held, and the pending mark is cleared only when a placement replaces it.

Written for commit 2c0ffae. Summary will update on new commits.

Review in cubic

@vieiralucas
vieiralucas requested a review from Copilot July 28, 2026 22:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vieiralucas
vieiralucas requested a review from Copilot July 28, 2026 22:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vieiralucas
vieiralucas requested a review from Copilot July 28, 2026 23:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vieiralucas
vieiralucas requested a review from Copilot July 28, 2026 23:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vieiralucas
vieiralucas requested a review from Copilot July 29, 2026 00:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vieiralucas
vieiralucas requested a review from Copilot July 29, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

… 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
vieiralucas force-pushed the core/c24-stamp-placement-collision branch from f20f1fa to 549656d Compare July 29, 2026 00:38
@vieiralucas
vieiralucas requested a review from Copilot July 29, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants