Skip to content

CORE-2: Map primitive + Element_id (UUID v5) + Value#5

Merged
vieiralucas merged 3 commits into
mainfrom
core-2-map-primitive
May 19, 2026
Merged

CORE-2: Map primitive + Element_id (UUID v5) + Value#5
vieiralucas merged 3 commits into
mainfrom
core-2-map-primitive

Conversation

@vieiralucas
Copy link
Copy Markdown
Member

@vieiralucas vieiralucas commented May 19, 2026

Summary

  • Adds Element_id (UUID v5 derived from (parent_id, key)), Value (closed sum: Scalar | Element), and Map (LWW per slot with Live/Tomb tombstones).
  • Wire-level Map ops trimmed to Set + Delete only. initOnce/replace were SDK guardrails that became dead surface once deterministic Element_id removed the concurrent-init orphan footgun.
  • apply returns (t', released_ids) — both displaced Live Element refs and stranded losers from losing Set ops with unique Element values. Doc layer decides orphan status by cross-checking reachability.
  • ARCHITECTURE.md updated: Map section rewritten around deterministic id, Map Slot Safety reframed, op kind enum trimmed.
  • Closes KANBAN CORE-2.

Modules

Module Surface Notes
Element_id root, derive ~parent ~key, to_string/of_string, to_bytes/of_bytes, compare, equal, pp UUID v5 via Uuidm.v5. Root = Uuidm.nil. Two clients computing same (parent, key) arrive at same id without coordination.
Value scalar = String | Int int64 | Float | Bool | Null; t = Scalar of scalar | Element of Element_id.t; pp/equal Blob constructor lands with the blob subsystem.
Map empty ~element_id, element_id, apply ~op ~op_id ~lamport -> t * released, get/keys/entries/cardinal, equal/pp LWW with (lamport, op_id) tiebreak. Tombstones survive Delete so stale Sets cannot resurrect deleted keys.

Released-ref semantics

Across both win and lose branches of apply:

  • Winner displaces Live Element with different id -> displaced id is released.
  • Winner displaces Live Element with same id (deterministic-SDK concurrent case) -> not released; id is still in slot via winner.
  • Loser is a Set with unique Element id -> stranded id is released.
  • Loser is a Set with same Element id as winner / a Scalar / a Delete -> nothing released.

Commits

  • d232125 CORE-2: Map primitive + Element_id (UUID v5) + Value
  • 2965b4d style: ocamlformat sweep (margin 90, break-infix fit-or-vertical)

Test plan

  • `dune build` clean
  • `dune runtest --force` -- 124 tests pass across 10 suites
    • element_id: 17 (root round-trips, derive determinism on (parent, key), nested chain, v5 version-nibble check, malformed/wrong-length rejects, 4 qcheck properties)
    • value: 11 (scalar variant equal+pp, t equal+pp, distinguishes Scalar vs Element)
    • map: 22 (empty, set LWW lamport+op_id tiebreak, delete LWW + survives stale Set, released_refs split into displaced/stranded/same-id/scalar cases, idempotency, concurrent deterministic convergence, keys/entries/cardinal sort, equal/pp)

Summary by cubic

Implements CORE-2 by adding a Map CRDT with deterministic Element_id (UUID v5 from (parent_id, key)) and reducing wire ops to map.set/map.delete so concurrent slot init converges without orphans. Also updates tests for qcheck-core API changes to keep CI passing.

  • New Features

    • Element_id: UUID v5 from (parent_id, key); root is nil. Same (parent, key) → same id across clients.
    • Value: Scalar (String, Int int64, Float, Bool, Null) or Element of Element_id.
    • Map: LWW per key with lamport/op_id tiebreak; deletes leave tombstones. apply ~op ~op_id ~lamport returns (t', released_ids) for displaced or stranded Element refs. SDK get-or-create helpers (e.g. Map.text key) derive the id and emit Set, so concurrent init converges.
    • Docs: Map section rewritten around deterministic ids; op kind enum trimmed to map.set and map.delete.
  • Migration

    • Emit only map.set and map.delete on the wire; initOnce/replace move to SDK sugar.
    • Handle released_ids from Map.apply in the doc layer to decide orphaning via reachability.

Written for commit 2f01725. Summary will update on new commits. Review in cubic

- Element_id: UUID v5 derived from (parent_id, key) via uuidm; root = nil
  (Uuidm.nil). Concurrent SDK helpers on same (parent, key) compute the
  same id, so LWW collapses to one winning Element with identical id on
  both sides -- no orphan.
- Value: closed sum -- Scalar (String | Int int64 | Float | Bool | Null)
  or Element of Element_id.t. Blob constructor lands with the blob
  subsystem.
- Map: Stdlib.Map.Make(String) of Live/Tomb slots; per-slot LWW with
  (lamport, op_id) tiebreak; tombstones survive Delete so stale Sets
  cannot resurrect a deleted key. Wire-level ops trimmed to just Set
  and Delete (initOnce / replace dropped -- they were SDK guardrails
  that became dead surface once deterministic Element_id removed the
  concurrent-init orphan footgun).
- apply returns (t', released_ids) -- both displaced Live Element refs
  and stranded losers from losing Set ops with unique Element values.
  Doc layer decides orphan status by cross-checking reachability.

ARCHITECTURE.md: Map section rewritten around the deterministic id +
Map Slot Safety reframed to drop replace/initOnce/throws guard. Closed
op kind enum updated to map.set | map.delete.

49 new tests across element_id (17), value (11), map (22). Total suite
now 122 tests across 10 binaries, all green.

Closes CORE-2.
.ocamlformat: margin 100 -> 90, add break-infix=fit-or-vertical.
Re-run ocamlformat across the tree to match.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 38 files

Re-trigger cubic

Local opam pins qcheck-core 0.25 where the top-level helper is
QCheck.string_of_size. CI installs a newer release where that helper is
renamed (QCheck.string_size) and the old name is a deprecated alert -
which CI's OCaml escalates to an error.

Use QCheck.make over Gen.string_size directly. Stable surface across
both versions, no deprecated alert.
@vieiralucas vieiralucas merged commit fba544b into main May 19, 2026
7 checks passed
@vieiralucas vieiralucas deleted the core-2-map-primitive branch May 19, 2026 21:59
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.

1 participant