fix(core): a children-list placement collision resolves by an intrinsic rank, not arrival order (C24) - #371
fix(core): a children-list placement collision resolves by an intrinsic rank, not arrival order (C24)#371vieiralucas wants to merge 1 commit into
Conversation
f20f1fa to
549656d
Compare
|
Held in draft: cold review pass 4 found a convergence hole in the rank this PR introduces, plus a regression. CI is green on H1 — the rank is still arrival-order dependent when the incumbent is the other derivable child of the key. Fix: rank on whether each side is the child this key derives, not on the arriving op's kind — H2 — the unconditional reparent in Also open, smaller: two |
|
Still held in draft. Cold review pass 5 found two more HIGHs, both reproduced with probes against the committed tree, plus three MEDIUMs. CI is green on every head so far, which is again the point — none of these is visible to a suite that only asks whether two arrival orders agree. H1 — a decode drops the created-under edge of a node whose birth placement is gone, and the replica then cannot load its own snapshot. H2 — two ops at one key naming the same node still resolve by arrival order. The rank is total over nodes, not over claims: when the element id ties, M1 — Pushed in this round: the self-claim direction and the birth test now assert which op wins rather than only that two orders agree — the gap that let an inverted comparison pass all 1733 tests. |
2faef55 to
e417325
Compare
…ic rank, not arrival order (C24) A children-list position 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 pass every gate — dedup is on `OpId`, and the id-space record only bounds an honest mint — so which of them takes the key had to stop being a function of delivery order. The key goes to a birth over a move, then to the smaller element id. A birth outranks because the key is where the born node's id comes from. The birth test reads the key rather than the registry, so a reveal under the other kind cannot invert it. A later-arriving winner evicts: the incumbent's placement goes, its move edge is withdrawn by the same undo-and-replay a late arrival uses, its reachability edge is re-derived from the surviving log, and a node left with no placement returns to awaiting its first, which is where a refusal leaves a claimant too. The sequence slot is re-seated rather than re-inserted, and that reaches the tombstones: a delete landing between the two ops would otherwise freeze the loser's anchor into the dead run on one replica and the winner's on the other. That rank orders nodes, which is the whole question only while two claims name two. Two moves of one node, or two inserts carrying one tag, hold one id between them, and the position was falling to arrival order — the first one's anchor for two moves, the last one's for two inserts. There the key is already the claimant's own and nothing changes hands: the sequence re-seats the id at the meet of the two anchors, which cannot show which landed first. A contest would have needed to know what put the incumbent at the key, and nothing answers that — the move log dedups on the stamp alone, so a move of another node at that stamp into another list takes the slot and the key-holder records no edge. The created-under relation is re-seeded before the log replay at both sites that rebuild it. A losing birth keeps that edge live but had none re-derived on decode, so the live replica refused a move under such a node as a cycle and then could not decode its own snapshot, while a replica started from those bytes admitted it and the node it took vanished. The projection re-fold empties the log and replays it as the last step before an encode, so a source missing there shipped a cyclic snapshot in a release build. The relation also runs through the map half one hop at a time, so the check reaches as deep as the nesting rather than one hop over the first map — the only part of this change that alters an honest document, recorded in ARCHITECTURE and DECISIONS. The four units C24 filed take C40-C43, the ids C15 (#368) had already merged having been taken.
e417325 to
db07f0f
Compare
A children-list position is keyed
(list, stamp)and a document holds at most one, sinceread_staterefuses a duplicate. Two ops can carry one stamp into one list and both pass every gate — dedup is onOpId, and the id-space record only bounds an honest mint — so which of them takes the key had to stop being a function of delivery order. TwoXmlMoves under oneClientIdat oneStampmoved different nodes on two replicas that folded the same ops, and theirencode_statebytes differed;XmlInsertChildreached it more quietly,xml_child_idmixing the kind in so a tagged and a tagless insert at one stamp derive different children that collide on the position without colliding on the element.The rank. The key goes to a birth over a move, then to the smaller element id — a total order over data both replicas already hold. A birth outranks because the key is where the born node's id comes from: a birth that lost would leave a node whose id names a position it does not hold and which nothing can re-derive. The birth test reads the key, not the registry, so a forged
XmlRevealunder the other kind cannot invert it.placement_indexcarries the owning node rather than being a set.Eviction restores the state the opposite order leaves. The incumbent's
Placementgoes; its move edge is withdrawn (TreeMoves::remove, by the same undo-and-replay a late arrival uses); its reachability edge is re-derived from the surviving log; and a node left with no placement returns to awaiting its first, which is where a refusal leaves a claimant too. The sequence slot is re-seated rather than re-inserted (insert_atis idempotent on the id), and that reaches the tombstones — a delete landing between the two ops would otherwise freeze the loser's anchor into the dead run on one replica and the winner's on the other, soreseatlifts the id out of its run, re-seats it and re-deletes.The rank orders nodes, which is the whole question only while two claims name two. A contest takes the winner's position outright — the loser's is nothing to it, and meeting the two would let the loser's show through exactly when it arrived first. Two moves of one node, or two inserts carrying one tag, hold one id between them and the position was still falling to arrival order — the first one's anchor for two moves, the last one's for two inserts, that second direction being new since re-seating replaced an idempotent
insert_at. There the key is already the claimant's own and nothing changes hands: the sequence re-seats the id at the meet of the two anchors, which cannot show which landed first. A contest would have needed to know what put the incumbent at the key, and nothing answers that —TreeMoves::applydedups on the stamp alone, so a move of another node at that stamp into another list takes the log slot and the key-holder records no edge. A move onto the key its own node was born at therefore keeps the edge it logs, where an earlier rule had the birth withdraw it; the edge names the parent the node was created under, so folding it moves nothing.The created-under relation is re-seeded before the log replay, at both sites that rebuild it. A birth records the list it was created in whether or not it takes that list's key, and a decode re-derived that edge only from a birth placement, which a losing birth does not have: the live replica refused a move under such a node as a cycle and then could not decode its own snapshot (
BadTag { parent cycle }), while a replica started from those bytes admitted the move and the node it took vanished.refold_projected_moveswas the quiet one — it empties the log and replays it as the last step before an encode, so a source missing there is adebug_assertin a test build and a cyclic snapshot in a release one.The map half of the tree anchors the cycle check, one hop at a time — a container keyed into a map records that map, and the map the element that owns it, so the walk runs as deep as the nesting rather than one hop over the first. This is the only part of the change that alters behaviour for an honest document; ARCHITECTURE and DECISIONS record it, with the cost stated: a small class of moves that used to apply is now refused as the cycle it always was.
Also.
refold_movesnow re-pointsparentsfor a node it finds deleted, instead of leaving that edge at whatever the previous fold wrote; a node's placement record is written key-sorted, since grown in arrival order it made two honest replicas encode different bytes. Speccrates/core/tests/xml_stamp_collision.rsfolds each shape in both orders and compares snapshot bytes, and names the winner in six tests so a flipped comparison fails by name rather than converging on the wrong answer.The tag two same-kind inserts at one stamp also name in common is still the first arrival's — the derivation carries no payload, by the purity the birth test needs — and a losing birth that kept a move placement has no created-under edge a snapshot can recover. Both are filed rather than folded in.
Six defects found while pinning this one are filed as C40-C45 (a plain-
ListInsertcollision; a reveal shell not surviving a snapshot; a create arriving after a reveal and a move; two moves at one stamp into different lists; the tag; the unrecoverable edge). The first four took C27-C30 at filing time and were renumbered, those ids being C15's.