Decision: keep yin stdlib-only.
Rejected alternative: pull in assertion, property-test, or CRDT helper packages.
Cost: tests and harness utilities are more verbose, and some ergonomics must be built locally.
Decision: design around deltas first while retaining whole-state merge for bootstrap, far-behind fallback, and the convergence oracle.
Rejected alternative: ship only operation/delta exchange or only whole-state merge.
Cost: implementations must maintain both paths and prove they agree.
Decision: causal context is keyed by yin-owned opaque ReplicaID, never by transport or peer identity.
Rejected alternative: reuse network peer IDs, addresses, or other transport identities as causal keys.
Cost: callers must maintain an explicit mapping from transport identity to ReplicaID.
Decision: order LWW timestamps by Lamport counter plus ReplicaID tie-breaker.
Rejected alternatives: HLC now, or wall-clock timestamps.
Cost: no physical-time ordering is exposed yet, and HLC migration requires preserving the timestamp abstraction.
Decision: use generic concrete CRDTs such as LWWRegister[T any] behind non-generic Document and Delta interfaces.
Rejected alternatives: all-any APIs, or fully generic interfaces throughout.
Cost: interface methods need type-safe concrete implementations plus non-generic adapter points.
Decision: keep the initial module in one root package, yin.
Rejected alternative: split primitives, documents, and test harnesses into subpackages immediately.
Cost: the root package carries more files until package boundaries are proven by real use.
Decision: build the convergence harness before real CRDTs and prove it fails against a deliberately broken merge.
Rejected alternative: write the register first and trust later tests to validate convergence.
Cost: the first slice includes toy test-only CRDTs and a temporary local contract that will be generalized later.
Decision: represent Timestamp with unexported fields for a uint64 Lamport counter and issuing ReplicaID; compare by counter first, then by the ReplicaID lexicographic byte order.
Rejected alternatives: expose timestamp fields directly, use a signed or platform-sized integer counter, or let the ReplicaID tie-break depend on insertion/map order.
Cost: callers must use constructors/accessors, and serialized forms must preserve the uint64 counter plus opaque ReplicaID until a future HLC migration is designed.
Decision: an LWW register retains only the winning value, so its delta-since-VersionVector path returns the current winner whenever that winner's causal context is not dominated by the caller's vector.
Rejected alternative: keep a full per-register assignment history so delta-since could replay every intermediate write beyond a caller's vector.
Cost: register catch-up is correct and convergent, but not history-complete; callers that need audit/history semantics must layer that above the LWW register.
Decision: make Delta a non-generic public seam with a stable Kind() type tag and Version() causal coverage, while concrete delta implementations carry the fully reconstructable payload. The core Document and Delta interfaces remain wire-format-agnostic and non-generic; any encoding lives in explicit codecs at the boundary rather than in these interfaces.
Rejected alternatives: put JSON/protobuf/bytes methods on Document or Delta, or expose only opaque concrete deltas without a public type tag.
Cost: codecs must map concrete delta kinds to chosen wire representations, and concrete CRDTs must keep their tags and reconstructable payloads stable enough for those codecs.
Decision: keep stdlib JSON delta codecs in the root yin package at the explicit delta boundary, with the typed DeltaCodec implementations as the only public JSON delta seam. Current root-package JSON support covers both LWWRegister[T] deltas and LWWMap[V] deltas: JSONLWWRegisterDeltaCodec[T] implements DeltaCodec for concrete register deltas, and JSONLWWMapDeltaCodec[V] implements DeltaCodec for concrete map deltas including top-level JSON object use.
History/state: the register codec established the first typed DeltaCodec seam; the later LWWMap JSON helpers (MarshalDeltaJSON, UnmarshalLWWMapDeltaJSON[V]) kept the same transport-agnostic boundary without turning JSON bytes into a general wire protocol; the map codec then made that seam symmetric for consumers that want a DeltaCodec value. Once the typed map codec existed, the helpers were removed pre-1.0 rather than retained: they duplicated the codec's job with a weaker, non-errors.Is error contract, and MarshalDeltaJSON read as general while supporting only map deltas via a hidden kind-switching interface — the start of the codec registry this decision rejects. These APIs are serialization seams only: they are not transport protocols, storage formats, whole-document serialization formats, full-state snapshot codecs, sync/session protocols, or codec registries.
Rejected alternatives: define a general wire protocol for all Document or Delta implementations, add a yin/codec subpackage or registry, tie codecs to a transport, serialize whole documents in these delta codecs, or keep the convenience helpers as a second entry point beside the typed codecs.
Cost: callers must choose the typed generic API that matches the CRDT/value type, route only supported deltas to it, and handle ErrUnsupportedDelta and ErrMalformedDelta at codec boundaries. Typed DeltaCodec implementations wrap encode failures with ErrUnsupportedDelta and decode failures with ErrMalformedDelta. Core Document and Delta application keep their existing format-agnostic ingest behavior.
Decision: Merge and ApplyDelta return changed bool, where true means the receiver advanced and false means stale, duplicate, empty, or otherwise no-op input. This is the single idempotent ingest signal replication consumers need.
Rejected alternatives: return no signal, return a status enum, or require callers to compare versions before and after every ingest.
Cost: callers that need a reason for a no-op must add their own diagnostics, and concrete implementations/codecs must decide how to handle incompatible or malformed inputs without expanding the core interface.
Decision: keep incompatible-type ingest as documentation-only behavior in the core interfaces: a false Merge or ApplyDelta result can mean incompatible type as well as stale, duplicate, empty, or otherwise no-op input. Callers should route only type-compatible documents and deltas to each ingest point.
Rejected alternative: add a debug/assert-mode signal for incompatible types now, deferred to a later slice.
Cost: incompatible-type wiring bugs are not surfaced at runtime by the core APIs, so callers that need diagnostics must add them around routing or codecs.
Decision: keep LWWRegister[T any] and compare register values with reflect.DeepEqual inside Equal.
Rejected alternative: constrain T to comparable so equality could use ==.
Cost: equality is runtime/reflection based and follows reflect.DeepEqual semantics, but registers can hold slices, maps, and other non-comparable value shapes.
Decision: make the convergence harness drive the public Document interface and mix whole-state Merge with ExtractDelta/ApplyDelta, using whole-state merge as the oracle while checking idempotent delta replay.
Rejected alternative: keep separate toy-only, merge-only, or register-specialized convergence tests.
Cost: the test harness carries scratch document factories and richer traces, but the same randomized run now proves convergence plus the sync catch-up and idempotent-ingest shapes.
Decision: the first JSON merge behavior targets top-level JSON object fields so independent root-field edits survive; nested objects, arrays, and scalars are opaque LWW payloads at their containing top-level key.
Rejected alternatives: keep whole-file JSON in a single LWWRegister, or implement a recursive JSON CRDT before the root-object path works.
Cost: concurrent edits inside the same nested value still conflict as a whole payload until recursive JSON semantics are designed.
Decision: implement a reusable generic LWWMap[V] keyed by string before JSON-specific helpers; top-level JSON object helpers will use LWWMap[json.RawMessage].
Rejected alternatives: build a JSON-only object map, or put field-level merge rules directly into a codec/app layer.
Cost: the core package gains another generic CRDT API and must define value/delta behavior independently of JSON helpers.
Decision: treat generic LWWMap[V] values as immutable once they cross the API boundary, while giving the primary JSON object path explicit copy semantics. LWWMap[json.RawMessage] clones raw JSON bytes on local Set, public Get/Entries, whole-state Merge, delta ExtractDelta, and delta ApplyDelta so callers cannot mutate JSON document state without a timestamped write.
Rejected alternatives: deep-copy all generic values, require a clone function in NewLWWMap, or document json.RawMessage as caller-immutable like every other mutable value. Go cannot deep-copy arbitrary V any values correctly without an application-provided policy, and adding a clone hook now would complicate the common immutable-value path before real use proves it necessary.
Cost: generic maps with mutable value types other than json.RawMessage still rely on caller immutability discipline, and LWWMap[json.RawMessage] pays byte-copy allocation costs at public and sync boundaries.
Decision: include per-key tombstones and a local Delete(key) operation from the start so deletes replicate, converge, and prevent older sets from resurrecting removed values.
Rejected alternatives: physically remove keys without tombstones, or defer delete support until after set-only maps ship.
Cost: deleted keys retain causal metadata until a future compaction policy exists.
Decision: keep JSON/delta codecs and helpers in the root package for now, matching the current single-package module and avoiding premature package boundaries.
Rejected alternatives: create a codec subpackage immediately, or tie codecs to a specific transport.
Cost: serialization files will live beside CRDT primitives until real usage proves a package split is worthwhile.
Decision: defer recursive JSON CRDT semantics; after the first top-level JSON object path, nested objects and arrays remain opaque values unless a later decision defines deeper merge rules.
Rejected alternatives: model nested objects and arrays as CRDTs in the first JSON slice.
Cost: only independent top-level fields survive automatically; deeper concurrent edits can still conflict at the containing field.
Decision: add a durable CRDT-state JSON snapshot codec for LWWMap[json.RawMessage] with public API names MarshalLWWMapSnapshotJSON and UnmarshalLWWMapSnapshotJSON(replica ReplicaID, data []byte). The snapshot is distinct from the visible JSON projection (MarshalJSONObject/ParseJSONObject) and from transport delta JSON. Its top-level object uses kind "lww-map-snapshot" plus version and records fields. version records document causal coverage; records preserve each field key, the json.RawMessage payload bytes for live values, the deleted/tombstone flag, the Lamport counter, and the writer ReplicaID.
Rejected alternatives: reuse visible JSON as durable persistence, encode snapshots as transport/delta messages, genericize all LWWMap[V] snapshots first, or add recursive JSON/container semantics now.
Cost: callers must choose the correct encoding for the job and pass their local ReplicaID when decoding. UnmarshalLWWMapSnapshotJSON reconstructs stored CRDT state under the caller-supplied local replica identity instead of replaying records as local writes, so restore preserves tombstones and causal metadata without manufacturing new local events.
Decision: current JSON delta and snapshot encodings emit and require a top-level integer formatVersion with value 1. This applies to JSONLWWRegisterDeltaCodec[T] payloads, JSONLWWMapDeltaCodec[V]/LWWMap delta JSON helper payloads, and LWWMap[json.RawMessage] snapshot JSON. Existing version fields remain causal VersionVector coverage and are not schema-format versions. Decoders reject missing, non-integer, or unsupported formatVersion values, including unversioned legacy payloads, while yin is experimental/pre-1.0.
Rejected alternatives: reuse the existing causal version field for schema gating, silently accept unversioned payloads, or introduce a codec registry/protocol negotiation layer before external usage proves one is needed.
Cost: JSON bytes produced before explicit format versioning are intentionally incompatible with the current decoders; callers must refresh any pre-1.0 persisted or fixture payloads. See the current JSON format reference for the contract; this decision records its rationale rather than defining its fields.