Initial Commit
This commit is contained in:
131
openspec/changes/crdt-completion/tasks.md
Normal file
131
openspec/changes/crdt-completion/tasks.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user