fix(core): a node's tag is a meet over the claims that named it, not the first arrival's (C44)
VI
3mo ago+985+985+012345678901234567890123456789-42-42-01234567890123456789
xml_child_id mixes the children list, the stamp and the kind into the derivation but never the tag, so XmlInsertChild { tag: Some(b"div") } and { tag: Some(b"span") } at one Stamp into one list derive the same XmlElement id. registered_handle's or_insert_with seated the first arrival's tag and no op ever rewrote it.
- Reproduced before fixing (the filing said "by inspection"): the two orders fold to
frag(a(),div())andfrag(a(),span()), andencode_statediffers byte for byte — the XML registry writes each node as its id plus its tag. - Mixing the tag into the derivation stays rejected: C24's placement rank leans on
born_atbeing pure in the key. - The smaller tag bytes take the node (
XmlElement::claim_tag) — the intrinsic rank C40 (#400) landed one layer down. A meet, so idempotent/commutative/associative: a replay ties, a third claim only lowers, a merge cannot answer by which side received. - Recoverable on reload with no new persisted state: a decoded tag is unranked (one tag per id, no contest at decode) and later ops can only tighten it.
- Four seats run the rank, not the one filed. The birth;
apply_reveal— anXmlRevealnames an arbitraryElementIdwith arbitrary tag bytes and returned early on an already-materialised node, so fixing the birth alone leaves reveal(aa)→birth(div) folding toaaand the reverse todiv;XmlElement::merge, whose tag was "identity, not state, so left untouched" — resolving it by which side received; andcreate_container, safe against itself (node_idfolds the tag in) but collidable by a reveal. - The rank runs on every claim, including at an id that already determines its tag: whether an id determines its tag is not a question an id answers, and the reveal holds neither the map nor the key to check against. Where it does, every honest claim carries that tag and the rank is a tie. Residue filed as C154.
- A live handle stays valid and observes the restatement — a handle is a view onto convergent state, not a snapshot.
- Measured, not assumed:
List::merge'sclaim_rankis built fromput_node_value, which writes a composite as its kind and id and never its tag, so two same-id different-tag elements always rank equal and take the fold arm — the sequence engine structurally cannot decide a tag contest.Map::merge's LWW arm converges on the tag (symmetric slot LWW); its detached deep clone is C134's shape. - Spec
crates/core/tests/xml_tag_collision.rs: 9 tests, every shape folded in both (or all six / all four) orders against snapshot bytes, plus a round-trip and a live-handle test. - ARCHITECTURE §Tree Moves + DECISIONS + KANBAN updated.
Summary by cubic
Fixes XML tag collisions by ranking tag claims: the smaller tag bytes win. Reveal shells are keyed by (node, tag); the server retag delivery trigger was reverted, so delivery is unchanged in this PR; replicas converge and encode identically (C44).
- Bug Fixes
- Added
XmlElement::claim_tagand run it where a claim can meet an existing tag: at birth/create_container, taggedapply_revealon an already‑materialised element, andXmlElement::merge. Paths with no incumbent (first materialisation, decode, detached deep clone) seat the tag directly. - Reveal shells derive
OpIdfrom(node, tag); the id is injective and separates tagless from empty‑tagged shells, so corrected shells no longer dedup away and readers converge with snapshots. - Reverted the server change that emitted a shell on retag; delivery stays as-is and will be handled separately. Docs now state identity is necessary but not sufficient for delivery.
- Meet semantics (idempotent, commutative, associative); reload‑safe with no new state; live handles observe tag restatements. Tests cover reload‑between‑claims, permutations, empty and non‑UTF8 tags; XML diff notes tag changes emit as replaces (conservative under lowered tags).
- Added