10 KiB
10 KiB
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 statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin 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.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. Drift here means a prior change archived without regenerating the manifest.
1. Spec authoring
- 1.1 Read
../suaveplan/enterprise/crdt.mdend-to-end - 1.2 Read
openspec/specs/crdt/spec.md(does not exist yet — confirm and start fresh) - 1.3 Read
openspec/AGENTS.mdSections 13 and 14 - 1.4 Draft
proposal.mdfromopenspec/templates/proposal.md - 1.5 Fill in
## Source mappingtable — every requirement cites a line range OR appears in the "no source line" table with justification - 1.6 Draft
specs/crdt/spec.mdfromopenspec/templates/spec-delta.md - 1.7 Every requirement in the spec delta carries a
**Verified by:**line naming concreteit(...)titles
2. Implementation — crdts/g-set/g-set
- 2.1 Create
packages/flow/crdt/src/crdts/g-set/g-set.tsexportingclass GSet<T>implementingCRDT<Set<T>, GSetState<T>> - 2.2 Add
GSetState<T>({ type: "g-set"; elements: T[] }) andGSetOptions<T>({ initialElements?: T[] }) totypes/crdt.types.ts - 2.3 Implement
add(value),has(value),value()(returnsSet<T>),state(),merge(remote)(set union),toJSON(),fromJSON(json),clone() - 2.4 For non-primitive
T, key byJSON.stringify(value)for dedup (matches upstream); rejectBigInt/ circular-reference values withCrdtError(GSET_INVALID_ELEMENT) - 2.5 Add
g-set.mdsibling doc (≥200 words, basic + advanced examples) - 2.6 Add subpath export
"./crdts/g-set/g-set"topackages/flow/crdt/package.json - 2.7 Re-export
GSetfromindex.tsandGSetState,GSetOptionstypes
3. Implementation — crdts/lww-map/lww-map
- 3.1 Create
packages/flow/crdt/src/crdts/lww-map/lww-map.tsexportingclass LWWMap<V>implementingCRDT<Map<string, V>, LWWMapState<V>> - 3.2 Add
LWWMapEntry<V>({ value, timestamp }),LWWMapTombstone({ timestamp }), andLWWMapState<V>({ type: "lww-map"; entries; tombstones }) types totypes/crdt.types.ts - 3.3 Constructor signature:
new LWWMap<V>(nodeId: string, options?: { clock?: Clock }); default clock usesDate.now() - 3.4 Implement
set(key, value, ts?),get(key),has(key),delete(key, ts?)(returnsboolean),keys(),value()(returnsMap<string, V>),state(),merge(remote),toJSON(),fromJSON(json),clone(),reset() - 3.5
setanddeleteSHALL pulltsfrom the injected clock when the optional argument is omitted - 3.6
setSHALL be a no-op when an existing entry has a strictly greater timestamp; tie-break usesnodeIdlexicographic comparison (matchesLWWRegister) - 3.7
deleteSHALL leave a tombstone with the supplied (or clock-resolved) timestamp; subsequentsetwith 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) withCrdtError(LWWMAP_INVALID_KEY) - 3.10 Add
lww-map.mdsibling doc (≥200 words, basic + advanced examples) - 3.11 Add subpath export
"./crdts/lww-map/lww-map"topackages/flow/crdt/package.json - 3.12 Re-export
LWWMapfromindex.tsandLWWMapEntry,LWWMapTombstone,LWWMapStatetypes
4. Error codes
- 4.1 Add
GSET_INVALID_ELEMENTandLWWMAP_INVALID_KEYtoerrors/crdt-error.tsCRDT_CODESregistry - 4.2 Verify they are registered against the shared
defineCodesregistry on module load - 4.3 Update
CrdtErrortype 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)thenc.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>>"—expectTypeOfassertion - 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>>"—expectTypeOfassertion - 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
.mdforg-set.tsandlww-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.mdpackage listing line for@suaveplan/crdtto mentionGSetandLWWMap
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, noerrorfindings - 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>andLWWMap<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-memoryMeterProvider - 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.mdif 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/containsg-counter,pn-counter,lww-register,mv-register,or-set,two-p-set,rga. Nog-set/directory. Nolww-map/directory.errors/crdt-error.tsdoes not exportGSET_INVALID_ELEMENTorLWWMAP_INVALID_KEY.- Package currently green:
bunx turbo lint typecheck test --filter=@suaveplan/crdtpasses (11 files, 145 tests).
Missing pieces:
crdts/g-set/g-set.ts(G-Set CRDT class + state types + tests + sibling.md).crdts/lww-map/lww-map.ts(LWW-Map CRDT class with tombstones + clock injection + tests + sibling.md).- 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.