Files
2026-05-27 23:29:53 +01:00

10 KiB
Raw Permalink Blame History

Tasks — CRDT completion (G-Set + LWW-Map)

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened.

0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Spec authoring

  • 1.1 Read ../suaveplan/enterprise/crdt.md end-to-end
  • 1.2 Read openspec/specs/crdt/spec.md (does not exist yet — confirm and start fresh)
  • 1.3 Read openspec/AGENTS.md Sections 13 and 14
  • 1.4 Draft proposal.md from openspec/templates/proposal.md
  • 1.5 Fill in ## Source mapping table — every requirement cites a line range OR appears in the "no source line" table with justification
  • 1.6 Draft specs/crdt/spec.md from openspec/templates/spec-delta.md
  • 1.7 Every requirement in the spec delta carries a **Verified by:** line naming concrete it(...) titles

2. Implementation — crdts/g-set/g-set

  • 2.1 Create packages/flow/crdt/src/crdts/g-set/g-set.ts exporting class GSet<T> implementing CRDT<Set<T>, GSetState<T>>
  • 2.2 Add GSetState<T> ({ type: "g-set"; elements: T[] }) and GSetOptions<T> ({ initialElements?: T[] }) to types/crdt.types.ts
  • 2.3 Implement add(value), has(value), value() (returns Set<T>), state(), merge(remote) (set union), toJSON(), fromJSON(json), clone()
  • 2.4 For non-primitive T, key by JSON.stringify(value) for dedup (matches upstream); reject BigInt / circular-reference values with CrdtError(GSET_INVALID_ELEMENT)
  • 2.5 Add g-set.md sibling doc (≥200 words, basic + advanced examples)
  • 2.6 Add subpath export "./crdts/g-set/g-set" to packages/flow/crdt/package.json
  • 2.7 Re-export GSet from index.ts and GSetState, GSetOptions types

3. Implementation — crdts/lww-map/lww-map

  • 3.1 Create packages/flow/crdt/src/crdts/lww-map/lww-map.ts exporting class LWWMap<V> implementing CRDT<Map<string, V>, LWWMapState<V>>
  • 3.2 Add LWWMapEntry<V> ({ value, timestamp }), LWWMapTombstone ({ timestamp }), and LWWMapState<V> ({ type: "lww-map"; entries; tombstones }) types to types/crdt.types.ts
  • 3.3 Constructor signature: new LWWMap<V>(nodeId: string, options?: { clock?: Clock }); default clock uses Date.now()
  • 3.4 Implement set(key, value, ts?), get(key), has(key), delete(key, ts?) (returns boolean), keys(), value() (returns Map<string, V>), state(), merge(remote), toJSON(), fromJSON(json), clone(), reset()
  • 3.5 set and delete SHALL pull ts from the injected clock when the optional argument is omitted
  • 3.6 set SHALL be a no-op when an existing entry has a strictly greater timestamp; tie-break uses nodeId lexicographic comparison (matches LWWRegister)
  • 3.7 delete SHALL leave a tombstone with the supplied (or clock-resolved) timestamp; subsequent set with a strictly greater timestamp clears the tombstone
  • 3.8 merge(remote) SHALL respect tombstones — remote entries older than a local tombstone are dropped; remote tombstones newer than a local entry remove the entry
  • 3.9 Reject non-string keys (e.g. BigInt-coerced) with CrdtError(LWWMAP_INVALID_KEY)
  • 3.10 Add lww-map.md sibling doc (≥200 words, basic + advanced examples)
  • 3.11 Add subpath export "./crdts/lww-map/lww-map" to packages/flow/crdt/package.json
  • 3.12 Re-export LWWMap from index.ts and LWWMapEntry, LWWMapTombstone, LWWMapState types

4. Error codes

  • 4.1 Add GSET_INVALID_ELEMENT and LWWMAP_INVALID_KEY to errors/crdt-error.ts CRDT_CODES registry
  • 4.2 Verify they are registered against the shared defineCodes registry on module load
  • 4.3 Update CrdtError type union to include the new codes

5. Tests — G-Set

  • 5.1 g-set.test.ts::"adds an element and reports has() true" — basic add / has
  • 5.2 g-set.test.ts::"value returns a Set with all current elements" — value snapshot
  • 5.3 g-set.test.ts::"merge is set union" — two G-Sets merge to combined elements
  • 5.4 g-set.test.ts::"merge is idempotent" — a.merge(b.state()); a.merge(b.state()) no different from one merge
  • 5.5 g-set.test.ts::"merge is commutative" — a.merge(b) then c.merge(a.state()) equals starting fresh and merging in any order
  • 5.6 g-set.test.ts::"merge is associative" — (a ∪ b) ∪ c === a ∪ (b ∪ c) over same input
  • 5.7 g-set.test.ts::"toJSON / fromJSON round-trip" — full state round-trip
  • 5.8 g-set.test.ts::"clone produces an independent copy" — mutation of clone does not affect original
  • 5.9 g-set.test.ts::"initialElements seeds the set" — constructor option populated correctly
  • 5.10 g-set.spec-conformance.test.ts::"GSet<T> is CRDT<Set<T>, GSetState<T>>" — expectTypeOf assertion
  • 5.11 g-set.spec-conformance.test.ts::"throws GSET_INVALID_ELEMENT for BigInt element" — error path

6. Tests — LWW-Map

  • 6.1 lww-map.test.ts::"set then get returns the value" — basic round-trip
  • 6.2 lww-map.test.ts::"set with explicit timestamp respects ordering" — older ts is no-op
  • 6.3 lww-map.test.ts::"tie-break on equal timestamp uses nodeId" — lexicographic comparison
  • 6.4 lww-map.test.ts::"delete leaves a tombstone" — get returns undefined, has returns false, internal tombstone present
  • 6.5 lww-map.test.ts::"set after delete with greater ts clears tombstone" — re-add path
  • 6.6 lww-map.test.ts::"merge respects tombstones — older remote entry dropped" — convergence property
  • 6.7 lww-map.test.ts::"merge respects tombstones — newer remote tombstone removes local entry" — convergence property
  • 6.8 lww-map.test.ts::"merge is idempotent / commutative / associative" — three property tests
  • 6.9 lww-map.test.ts::"toJSON / fromJSON round-trip preserves entries and tombstones" — full state round-trip
  • 6.10 lww-map.test.ts::"clone produces an independent copy" — mutation of clone does not affect original
  • 6.11 lww-map.test.ts::"reset clears entries and tombstones" — empty state restored
  • 6.12 lww-map.test.ts::"keys returns active keys excluding tombstones" — listing semantics
  • 6.13 lww-map.spec-conformance.test.ts::"LWWMap<V> is CRDT<Map<string, V>, LWWMapState<V>>" — expectTypeOf assertion
  • 6.14 lww-map.spec-conformance.test.ts::"set without ts pulls from injected clock" — deterministic clock injection
  • 6.15 lww-map.spec-conformance.test.ts::"throws LWWMAP_INVALID_KEY for non-string key" — error path
  • 6.16 Every it(...) in §5 and §6 carries { timeout: 5000 }

7. Documentation

  • 7.1 Sibling .md for g-set.ts and lww-map.ts (≥200 words each, basic + advanced examples)
  • 7.2 Update packages/flow/crdt/README.md — add Modules links for the two new files; add Quick-start snippets for both
  • 7.3 Update CLAUDE.md package listing line for @suaveplan/crdt to mention GSet and LWWMap

8. Pre-archive verification (the 5-gate triple)

  • 8.1 bun run check — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
  • 8.2 bunx turbo lint typecheck test --filter=@suaveplan/crdt — green
  • 8.3 bunx turbo test:coverage --filter=@suaveplan/crdt — 100/100/100/100
  • 8.4 bun scripts/openspec-conformance.ts @suaveplan/crdt — exit 0, no error findings
  • 8.5 bunx turbo build --filter=@suaveplan/crdt — green

9. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 9.1 Generics threaded through GSet<T> and LWWMap<V>
  • 9.2 Zod schemas wired at any declared boundary if a CRDT envelope is exposed across a network edge (likely deferred — CRDT-side serialisation is JSON-only here)
  • 9.3 Every error code in errors/crdt-error.ts (including the two new codes) thrown by at least one production path
  • 9.4 Strict-mode behaviors tested both on and off (tie-break with equal ts ON/OFF for nodeId comparison)
  • 9.5 OTel instruments emitted from production paths (counters for gset.add, lwwmap.set, lwwmap.delete, lwwmap.merge.entries, lwwmap.merge.tombstones) and asserted with a real in-memory MeterProvider
  • 9.6 Lifecycle dedup not applicable here — CRDTs are stateful instances, no shared registry
  • 9.7 No retroactive tick — every box above reflects work done in this change

10. Archive

  • 10.1 Fold spec delta into openspec/specs/crdt/spec.md (will be the first time this canonical spec exists)
  • 10.2 Move openspec/changes/crdt-completion/ → openspec/archive/<yyyy-mm-dd>-crdt-completion/
  • 10.3 Refresh MANIFEST.md if package set or deps changed (no expected change here)

Audit Note (2026-05-02)

Audit verdict: DEFERRED — 0/74 tasks evidenced in code.

Evidence reviewed:

  • packages/flow/crdt/src/crdts/ contains g-counter, pn-counter, lww-register, mv-register, or-set, two-p-set, rga. No g-set/ directory. No lww-map/ directory.
  • errors/crdt-error.ts does not export GSET_INVALID_ELEMENT or LWWMAP_INVALID_KEY.
  • Package currently green: bunx turbo lint typecheck test --filter=@suaveplan/crdt passes (11 files, 145 tests).

Missing pieces:

  1. crdts/g-set/g-set.ts (G-Set CRDT class + state types + tests + sibling .md).
  2. crdts/lww-map/lww-map.ts (LWW-Map CRDT class with tombstones + clock injection + tests + sibling .md).
  3. New error codes (GSET_INVALID_ELEMENT, LWWMAP_INVALID_KEY) and the OTel counters listed in §9.5.

Decision: leave change open. Implementing both CRDTs with full test parity (idempotent / commutative / associative property suites + spec-conformance) is real implementation work, not an audit-pass tick.