CRDT Merge Strategies
stratt advanced 7 min read
ELI5
When two editors change the same unit, STRATT looks up which field they touched and applies that field’s house rule: meta uses last-write-wins, contracts demand a human, lists union, and the fingerprint is always recomputed at the end.
Technical Deep Dive
Strategy Routing
flowchart TB E["applyEdit(doc, edit)"] --> R{"field type"} R -->|"meta"| LWW["last_write_wins"] R -->|"contract"| MAN["manual_resolution"] R -->|"steps"| APP["append_wins"] R -->|"imports"| UNI["union_merge"] R -->|"status"| HRW["highest_restriction_wins"] R -->|"id, slug, created"| IMM["immutable (reject)"] LWW --> FP["recompute_post_merge<br/>→ new fingerprint"] APP --> FP UNI --> FP HRW --> FP MAN --> FPStrategy Catalogue (packages/crdt/src/strategies.ts)
| Strategy | Applies To | Behaviour |
|---|---|---|
last_write_wins | meta fields | Latest timestamp wins |
manual_resolution | contract | Surfaces a conflict; blocks publish until resolved |
append_wins | composition.steps | Both edits append; order preserved |
union_merge | imports | Set union; duplicates collapsed |
highest_restriction_wins | status | Picks most restrictive per AC-02 ordering |
recompute_post_merge | meta.fingerprint | Always runs last; never user-supplied |
immutable | id, slug, domain, type, created | Rejects the edit |
Restriction Order (AC-02)
tombstoned > archived > deprecated > published > active > approved > review > draft
A merge of published and deprecated resolves to deprecated; merging tombstoned with anything yields tombstoned.
applyMergeStrategy Signature
applyMergeStrategy(doc, field, current, incoming, strategy, edit) → MergeResult { type, value } — pure function, no side effects on the Yjs doc until the caller commits.
Key Terms
- PromptUnitDoc → the Yjs document type wrapping a unit’s mutable fields.
- Manual resolution → a conflict that must be resolved by a human reviewer; blocks
publish. - Recompute post-merge → fingerprint is derived, never merged.
Q&A
Q: Why is created marked immutable rather than last_write_wins?
A: created is part of canonical identity; mutating it would silently change every downstream fingerprint and break content-addressing.
Q: Can a merge produce a unit that fails Zod validation?
A: Yes — append_wins on steps can introduce an invalid composition. The validator runs after the merge and the unit lands in a tampered-adjacent state until corrected.
Examples
Editor A renames a step argument while Editor B appends a new step. contract triggers manual_resolution (blocking); steps resolves via append_wins; recompute_post_merge then issues a new blake3: digest.
neighbors on the map
- Blake3 Fingerprint API verifying a unit hasn't been tampered with