# Collaborative Document Architecture — Design Spec (server-centric)

> **Superseded by `docs/single-author-playbook.md` (2026-08-28).** Kept as history.

Design specification for collaborative editing in a Rust diagram editor
(immediate-mode UI, ~10^5 elements, natural "commit points" at gesture
boundaries).

**Pivot, 2026-08-13.** This spec previously described a local-first, causal
design: Lamport clocks, per-actor stamps, a content-addressed change DAG, and
peer merge. That design is superseded — the previous spec text lives in git
history (through commit `b8b73d7`). The project is now **server-centric**:
a central server owns the document and defines the total order of edits by
arrival; clients are edit surfaces that predict server state optimistically
and reconcile against its replies. This is the "server-sequenced / hosted
(Figma-shaped)" branch of the two-orderings analysis in
`docs/doc-ng-design-notes.md`, chosen deliberately: it deletes the
clock/DAG/hash machinery at the cost of requiring a server, and the cost is
accepted (a later "local mode" can embed the server in-process — the
authority model is unchanged, only the transport shortens).

This document records **decided architecture**. The migration that implements
it is `docs/collab-migration-playbook.md`; the step-by-step manual merge of
`src/log` into `src/doc_ng` is `docs/doc-ng-port-playbook.md` (complete
2026-08-16; `src/log` deleted).

---

## 1. Core decisions (summary)

| Concern | Decision |
|---|---|
| Topology | **Client/server.** The server owns the change log and the folded document; clients own the edit surface, the undo stack, and an optimistic prediction of server state |
| Document representation | Append-only **linear log of commits** (batches of typed ops), sequenced by the server |
| Total order | **Server arrival order.** Each accepted commit gets the next `Rev` (u64, contiguous from 1). The per-write order is `WriteOrder { rev, seq }` — `seq` is the op's index within its commit |
| Merge strategy | All state is **atomic LWW registers**; last write in server order wins; nothing is rejected on conflict |
| Conflict engine | None needed — the server's sequencing *is* the resolution. No DAG, no clocks, no content addressing, no merge/repair pass |
| Global invariants | Enforced by **server-side validation at ingress** (the server folds every commit before sequencing it; invalid ⇒ rejected, never sequenced) |
| Ordering (z-order) | Derived: chronological by max write order, within the fixed cross-kind layer rank; no order-valued state |
| Text | Atomic (whole-string LWW register); no character-level merging (v1) |
| Lists (waypoints) | Atomic (whole-list LWW register) |
| Containment | Strict tree; parent is an LWW register; cycle-creating writes are refused at server ingress |
| Identity | Random UUIDv4 per entity at creation, never reused; tombstones on delete. Commits are identified by `Rev` — no hashes |
| Attribution | None in v1. `ActorId` is deleted; if attribution is wanted later it is a server-side column on the commit row (who was connected), never merge state |
| Transport | **WebSocket** (axum server side; `ewebsock` or equivalent client side), CBOR binary frames (2026-08-16) |
| Server storage | **SQLite via rusqlite**, server-side only. One table of `(rev, wall_time, payload)` rows. Clients persist nothing |
| Server role | **Folds and validates.** The server maintains the folded `Document` via the same shared fold the clients run, and refuses commits that fail |
| Undo | Client-owned, journal-based: displaced values are captured at seal time; undo submits the stored inverse as an ordinary commit. `old` values never cross the wire |
| Crate layout | Cargo workspace: shared document crate (model, fold, commits, protocol), server binary crate, egui client crate |
| Web/wasm client | Deferred. Native clients first; the client networking choice (`ewebsock`) keeps the wasm door open |

Prior art: Figma multiplayer (per-property LWW, server-ordered, client-side
prediction and per-user undo) is now the *design*, not just an influence.
The CRDT literature and the causal/DAG design remain documented in
`docs/doc-ng-design-notes.md` ("Two orderings") as the road not taken and
why.

---

## 2. Topology and ownership

```
                    ┌─────────────────────────────┐
                    │  server (blockworx-server)  │
                    │                             │
   SQLite  ◄──────► │  log: Vec<(Rev, Commit)>    │
   (rev, payload)   │  state: Document  = fold(log)
                    │  next rev, broadcast fanout │
                    └────────▲──────────┬─────────┘
                    Submit   │          │  Committed / Rejected (to sender)
                    {nonce,  │          │  Apply { rev, commit } (to others)
                    commit}          ▼
              ┌─────────────────────────────────────┐
              │  client (blockworx, egui)           │
              │                                     │
              │  confirmed: Document = fold(log ≤ rev)
              │  pending:   FIFO of unacked commits
              │  optimistic: confirmed ⊕ pending    │  ← what the editor reads
              │  undo stack (client-local)          │
              └─────────────────────────────────────┘
```

The ownership line, stated once: **the server owns document state; the
client owns the edit surface.** Everything the server holds is derivable
from its SQLite log; everything the client holds is derivable from the
server's log plus its own unacknowledged commits. A client that crashes
loses nothing but its undo stack; a client that reconnects refetches and is
whole.

---

## 3. Layered representations (strict one-way derivation)

```
Layer 1: Commit log (linear, server-sequenced)  ← the ONLY authoritative representation
Layer 2: Document state (entities + registers)      = fold(log)   — held by server AND clients
Layer 3: Derived content (auto-routes, solver output, caches)     — regenerated, never sent
Layer 4: Render/interaction caches (spatial index, draw lists)    — ephemeral, client-only
```

Rules (unchanged from the original spec — the pivot does not touch them):

- Nothing in layers 2–4 is ever authoritative. Any state/log divergence
  resolves in favor of the log.
- The fold is a pure deterministic function: no I/O, no clocks, no
  randomness, no re-executed logic. The **same fold code** runs on the
  server (validation + state) and on every client (confirmed + optimistic)
  — one implementation in the shared crate, so the two cannot drift.
- Derived artifacts are not referenceable by ops.
- Cold start = fetch log, fold. Snapshot-then-tail is a later server-side
  optimization, not a correctness feature.

---

## 4. Data model (Rust)

The source of truth for the model is `src/doc_ng/` (typed per-kind entities,
registers with inline write orders, per-kind update/init vocabularies — see
`docs/doc-ng-design-notes.md`). The ordering and envelope types after the
pivot:

```rust
// Newtypes everywhere — never bare u64/u32.
struct Rev(u64);      // server-assigned commit sequence number; contiguous from 1
struct Seq(u32);      // an op's index within its commit

/// The register comparison key — the total order on writes.
/// Rev::ZERO + Seq::FIRST = BOTTOM, below every accepted write.
struct WriteOrder { rev: Rev, seq: Seq }

/// One commit: the unit a client seals, the server sequences, and undo
/// inverts.
struct Commit {
    label: String,        // semantic label: "Moved 15 elements"
    ops: Vec<OpCodes>,    // ordered batch; an op's index is its Seq
}

/// The wire and storage form. Versioned: a V2 from a newer build fails
/// to deserialize rather than being reinterpreted. Hosts unwrap it and
/// validate/fold the `Commit` inside.
enum CommitEnvelope { CommitV1(Commit) }
```

What is *not* in the envelope, deliberately:

- **`Rev`** — assigned by the server after the client seals the payload;
  it travels beside the payload on the wire and is the primary key in
  storage, never a field of the thing it orders.
- **`wall_time`** — stamped by the server at acceptance (UTC millis, one
  clock for the whole document), stored as a column beside the payload.
  Display metadata, never load-bearing.
- **`nonce`** — transport-level ack correlation (per-connection counter),
  never persisted.
- **parents / hashes / stamps / actor** — deleted by the pivot. A linear
  server-ordered log has no branches to name, no causality to carry, and
  no identity beyond `Rev`.

`OpCodes` / `Crud<I, U>` / the per-kind `*Update` vocabularies and `*Init`
structs keep their shape (`src/doc_ng/opcode.rs`, `operands.rs`): typed
along the entity-kind axis, total init structs, zero-value principle, no
`Option`. One slimming (decided 2026-08-13): **update variants carry only
the new value — `old` never crosses the wire.** `Swap { old, new }` leaves
the vocabularies; replay never read `old`, and its one real consumer —
undo — captures baselines client-side into a journal at seal time (§10).

### Compound-operation rule (critical, unchanged)

High-level operations (align, auto-route, duplicate, cascade delete) are
recorded as the **primitive ops they produced** — concrete values,
pre-minted UUIDs, pre-remapped references — under one commit label.
Replay is a dumb fold; nothing re-executes.

---

## 5. LWW under server order

The entire merge rule, per register, unchanged in shape:

```rust
fn apply(&mut self, value: T, order: WriteOrder) -> Applied {
    if order > self.order { self.value = value; self.order = order; Applied::Won }
    else { Applied::LostToNewer }
}
```

Where totality comes from after the pivot: **the server never reuses a
`Rev`** (a single-writer task incrementing a counter, persisted as an
`INTEGER PRIMARY KEY`), and `Seq` separates writes within one commit.
That one sentence replaces the entire clock-resume invariant, the
per-actor lamport discipline, and the ingest assertions of the causal
design — the arbiter is a process, not a protocol.

In a strictly forward fold of a linear log every apply wins by
construction, so the key comparison looks redundant. It is kept because it
is what makes the register a self-contained join (`max` over a total
order) rather than a function of delivery discipline:

- duplicate delivery (a resync overlapping live traffic) is a structural
  no-op, not a protocol obligation;
- the client's optimistic layer re-applies pending commits on top of a
  moving confirmed state, and the key is what keeps that a pure
  recomputation;
- a commit that writes one register twice resolves last-wins by `seq`,
  deterministically, however it is replayed.

**Two clients concurrently writing one register:** the commit that
reaches the server later wins — last-write-wins in server order. Nothing
is rejected on conflict; conflict *rejection* (first-write-wins with a
base-rev check) was considered and declined for v1. The losing client sees
its value displaced when the winning commit arrives — the same
experience as watching a collaborator edit.

---

## 6. The client session

The client holds three things:

```rust
struct ClientSession {
    confirmed: Document,           // fold of the server log; its rev is the head
    pending: VecDeque<Pending>,    // sealed, submitted, not yet acked; FIFO
    optimistic: Document,          // confirmed ⊕ pending — what the editor reads
}
struct Pending { nonce: Nonce, commit: Commit }
```

(Amended 2026-08-15/16: the head `Rev` is a field of `Document`, minted
only by a successful `try_apply`, so the session carries no separate
`confirmed_rev` — see the design notes, "The rev lives in the document".
The *kind* of head is part of the document's type: `confirmed` is a
`Document<Confirmed>`, `optimistic` a `Document<Provisional>` whose rev
is scratch and has no accessor. Anything reporting a rev to the server
therefore reads `confirmed`'s because it is the only one it can read.)

Rules:

1. **An edit** seals a commit at a gesture boundary, applies it to
   `optimistic` immediately (same frame — immediate-mode responsiveness is
   unchanged), pushes it on `pending`, and submits it. The client never
   assigns a `Rev`; pending commits get *provisional* write orders — the
   rebuild's successive folds mint them, one rev per pending commit above
   the confirmed head — that exist only inside this client.
2. **`Apply { rev, commit }`** (someone else's edit): apply to
   `confirmed` at `rev`, then rebuild `optimistic = confirmed ⊕ pending`.
   Rebuilding re-applies the pending commits at new provisional orders
   above the new confirmed head — this is the Figma reconciliation:
   unacknowledged local values keep suppressing conflicting incoming ones,
   because they sort later.
3. **`Committed { nonce, rev }`** (own edit acked): pop the front of
   `pending` (its nonce must match — FIFO ordering is guaranteed per
   connection), apply that commit to `confirmed` at `rev`, rebuild
   `optimistic`.
4. **`Rejected { nonce, reason }`**: drop the pending commit, rebuild
   `optimistic` (the edit visibly reverts), surface the reason, and
   resync. Rejection is rare — a validation race the client could not see
   (§8: e.g. a reparent that became a cycle because a concurrent move
   landed first) or a bug — so it gets this one plain handled path, not a
   designed UX flow.
5. **Reconnect** = fresh `Welcome` (refetch the full log, refold, replay
   pending). Incremental resume (`after: Rev`) is a later refinement.

The rebuild in rules 2–3 clones `confirmed` and re-folds `pending`.
`pending` is at most a few gestures deep (bounded by server round-trip
time, not by document size), so the cost is a document clone per incoming
commit — and the document's interior is `Arc`'d with entity-level
copy-on-write, so a clone is a pointer bump per entry. Acceptable for the
prototype; if profiling ever objects, the optimization is applying
foreign commits directly to `optimistic` when no pending write targets
the same registers — an optimization, never a second source of truth.

**A pending commit can fail to re-fold** on a moved confirmed head: a
foreign reparent can turn a pending reparent into a cycle (the only
reachable case — entries never leave the maps, so a foreign commit
cannot make a reference dangle). The rebuild **skips it and leaves
`pending` untouched**: the queue records what is in flight and is
mutated only by rules 3–4, while the rebuild is a per-frame prediction.
It is never re-submitted. Per-connection FIFO makes the prediction
correct by the time the answer arrives — the submitter's stream carries
every sequenced commit in rev order, so the client has folded exactly
what the server folded before validating it, and if a later commit
removes the cycle the edit repaints before the server accepts it.

---

## 7. Wire protocol

**CBOR binary frames** over one WebSocket per client (decided
2026-08-16, superseding JSON text frames). One codec serves the wire and
the server's log: the wire is transient and the log eternal, but that
difference is about tolerated version skew, not encoding, and a second
codec would be a fork that drifts. CBOR is self-describing and tagged by
name, which is what makes decode-forever tractable — field order is
free and an added variant disturbs no stored payload. Legibility on the
wire is given up; `Debug`/`Display` serve that better anyway, and
`ciborium::Value` decodes a frame for inspection. Ordered, exactly-once
delivery per connection is assumed (that is what TCP + WS gives); loss of
the connection is handled by reconnect-and-refetch, not by protocol-level
retransmission.

```rust
enum ClientMsg {
    Submit { nonce: Nonce, commit: CommitEnvelope },
}

enum ServerMsg {
    /// On connect: the full log. The client folds it and opens the editor.
    Welcome { rev: Rev, commits: Vec<(Rev, CommitEnvelope)> },
    /// To the submitter: your commit is sequenced as `rev`.
    Committed { nonce: Nonce, rev: Rev },
    /// To the submitter: refused at validation; nothing was sequenced.
    Rejected { nonce: Nonce, reason: String },
    /// To everyone else: an accepted commit.
    Apply { rev: Rev, commit: CommitEnvelope },
}
```

Invariants: `Committed`/`Apply` revs are contiguous per client — each
client can assert `rev == confirmed.rev().next()` on every message,
*before* folding, and treat a gap as a protocol bug (crash in debug,
resync in release). The server
sends `Welcome` from its own single-writer task, so no commit can fall
between the snapshot of the log and the start of the subscription.

---

## 8. Global invariants: validate at ingress, not repair after merge

The causal design needed a merge-repair pass (cycle voiding, dangling-ref
sweeps) because histories merged *after* both sides had committed. Under
server sequencing there is exactly one apply point, so **every global
invariant becomes a precondition checked by the server before a commit
is sequenced**:

- The shared crate exposes `validate(&Document, &Commit) -> Result<()>`
  — reference existence, kind agreement, containment acyclicity, geometry
  bounds. The server runs it against its folded state; a failure is a
  `Rejected`, and the log never contains an invalid commit.
- Clients run the *same* `validate` against `optimistic` before
  submitting, so a rejection can only arise from a race the client could
  not see (e.g. a reparent that became a cycle because of a concurrent
  move that landed first) — rare, and handled by the rejection path.
- **A well-behaved client can otherwise never be rejected**: every id it
  references was learned from the server's log, creates mint fresh UUIDs,
  and deletes are tombstones (the entity remains addressable), so
  reference-existence cannot fail from concurrency alone.

One derivation survives from the repair world because it is a *view* rule,
not a state mutation: a route whose endpoint pin is tombstoned is
**suppressed** (`DocumentCache::suppressed` — rendered as deleted, revived
automatically when the endpoint is restored). Edits racing deletes land in
the tombstone's retained inner, exactly as before — delete-wins is still
structural (edit ops cannot target presence).

The decode boundary remains a trust boundary — more so now that the server
ingests payloads from arbitrary clients: deserialization validates
(range-checked geometry, unknown-variant refusal), never normalizes.

---

## 9. Move vs duplicate (cut/copy/paste) — unchanged

- Move is identity-preserving: one parent-register write. Never
  delete+recreate.
- Cut + first paste = move; copy-paste and later pastes = duplicates with
  fresh UUIDs and remapped internal references.
- Clipboard holds a value snapshot, not live refs.
- Concurrent double-move of one subtree = two writes to one parent
  register ⇒ LWW resolves in server order.

---

## 10. Undo/redo

- **The undo stack lives in the client**, as a journal: when the session
  seals a commit, it first reads the values the commit's updates
  displace from its optimistic document and stores the ready-made inverse
  commit (inverse ops in reverse order — a commit may write one
  register twice). Undo submits that entry as an ordinary new commit
  (append-only — history is never mutated); redo entries are rewritten
  with current values at undo time.
- Nothing journal-shaped appears in protocol messages: update ops on the
  wire carry only the new value. Lifecycle ops invert baseline-free —
  `invert(Create) = Delete`, `invert(Delete) = Restore` (the tombstone
  retains the inner values; that is what it is for).
- An undo can displace a collaborator's newer concurrent value — it is
  just a new write that wins LWW. That is accepted (Figma behaves the
  same). What must *not* happen is undo/redo round trips corrupting state:
  **the Figma round-trip invariant** — undo a lot, copy, redo back to the
  present ⇒ the document is unchanged — is adopted as a property test when
  undo lands, and its mechanism (rewriting the redo entry with current
  values at undo time, and vice versa) is the sanctioned use of
  capture-at-use-time: local session journal only, never the wire.
- Property test: `fold(fold(s, c), journal_inverse(c)) == s` on the
  user-visible projection, with the edit asserted observable.

---

## 11. Storage (server-side)

SQLite via rusqlite, in the server crate only. The whole schema:

```sql
CREATE TABLE commits (
  rev       INTEGER PRIMARY KEY,   -- server-assigned; contiguous from 1
  wall_time INTEGER NOT NULL,      -- UTC millis at acceptance; display only
  payload   BLOB NOT NULL          -- CBOR CommitEnvelope
);
```

- The server is handed a database path at startup; a missing file is
  created as an empty document (rev 0, no rows). On start it folds every
  row in rev order — a row that fails to decode or fold is a hard startup
  error, not a skipped commit.
- **Resolved 2026-08-16 (user decision): the column holds CBOR**, the
  same bytes the wire carries. One representation, one set of decode
  goldens. The cost, accepted with the decision: rows are opaque to
  `sqlite3` and `ripgrep`, so the "an agent can answer *when did this
  route change* without the app" goal now needs a tool that decodes —
  a `blockworx log` dump command, which gives structured queries rather
  than grep and is strictly more capable, but which nobody has written
  yet. Until it exists, that goal is unserved; it is a follow-up, not a
  thing the storage design forgot.
- Append is one `INSERT` inside the single-writer task; rev assignment and
  persistence cannot race because there is exactly one writer.
- Snapshots (`snapshot` table: fold cached at a rev, so `Welcome` sends
  snapshot + tail instead of the whole log) and an `element_index` blame
  table are later, additive optimizations. Design them so
  `debug_assert!(snapshot == replay)` guards every snapshot load.
- Deep-history compaction stays possible (drop rows below a baseline
  snapshot) but is out of scope until someone needs it.

Clients persist nothing. The `.bwx` container, autosave, and the timeline
tool continue to serve the legacy document path until the editor swap, and
their history role is then subsumed by the server log.

---

## 12. Serialization longevity

Bytes are **no longer identities** (nothing is hashed), so byte-exact
encoding stability is no longer load-bearing. What remains load-bearing is
**decode-forever**: every payload ever written to a server database must
decode in every future build.

- The envelope is a version-per-variant enum (`CommitEnvelope::CommitV1(...)`) — a
  `V2` from a newer build fails to deserialize in an old one rather than
  being reinterpreted. Adding an update-enum variant needs no new version:
  old builds refuse it, new builds read old logs.
- serde variant names are the wire tags: never rename or repurpose a
  variant; deprecate and add.
- Golden tests pin **decode compatibility**: a fixture file of encoded
  commits — one instance of every op variant of every kind — that every
  future build must decode to the same values (plus unknown-variant and
  future-version refusal tests). Byte-exact *encode* goldens are no longer
  required.
- **Determinism is still mandatory** — not for hashing, but because the
  server's fold and every client's fold must agree exactly. No floats in
  authored state (`FracVal` fixed-point everywhere; NaN refused at the
  boundary), ordered collections in anything the fold iterates.

---

## 13. Presence, leases, permissions (future)

All trivially server-side now, and all deferred:

- **Presence** (who is connected, live cursors) is a broadcast of
  ephemeral per-connection state — never document state.
- **Leases/locks** ("A is editing scope S") become a server-side check at
  ingress — a dozen lines in `validate`'s caller — instead of the
  advisory/soft/hard policy tower the causal design needed. Deferred until
  wanted.
- **Auth** is a connection concern (token at WS upgrade). Out of scope.

### 13a. Gesture streaming and the tutorial ghost (design note, 2026-08-19)

Recorded from design review; still deferred, but the shape is settled
so nothing built now forecloses it.

**The wire sees only sealed gestures — by design.** One commit per
gesture is what makes undo an inverse commit, gives review a semantic
label, and keeps the log and the merge small. A remote collaborator
therefore sees your block *jump* at `DragStopped`, not slide. Figma
makes the other choice (property updates streamed live during a
manipulation, throttled LWW per property, undo coalesced client-side);
if that experience is ever wanted here, the answer is **not**
micro-commits — that would shred the seal discipline and the undo unit
— but a **presence-channel extension**: ephemeral gesture messages,
never logged, never undoable, never folded.

**The symmetry that makes it cheap.** Editor-swap steps 7/8 drew the
authored-vs-ephemeral line locally: document versus presentation
(`src/presentation/` — previews compute against hypothetical state and
never touch the document). A gesture stream is the same line drawn
across the network, and the message vocabulary already exists: the
preview funnel's hypothetical-geometry types (`GeometryOverrides`,
`PinSlotOverride`, `RouteEditSession`, drag offsets). A remote gesture
preview is the local preview machinery fed by remote parameters
instead of the local pointer, rendered as a presentation-layer
overlay.

**The tutorial unification.** Once gesture streaming exists, a
tutorial demo ghost *is* a synthetic collaborator on the presence
channel — "watch a teammate resize" and "watch the tutorial
demonstrate resizing" become one rendering path with zero
tutorial-specific display code. Until then the tutorial player keeps
driving the real tools with synthetic events, which is what the golden
replays verify; commits deliberately cannot replace the recordings —
a commit says *what* changed, never *how*, and the "how" is both the
pedagogy and the verification value.

**Near-term harvest (independent of the channel; scheduled with the
flag-day series):**

1. *Session-isolated tutorial levels* — entering a tutorial opens a
   second `LocalHost` seeded from the level's lowered log; exiting
   drops it; restart re-welcomes the same commits. The user's own
   session is never touched, and the document swap/restore machinery
   (`exit_tutorial`'s rebuild) dies instead of being re-pointed.
2. *Commit-driven cue advancement* — tutorial step completion becomes
   a predicate over the incoming commit stream (typed ops + labels:
   "a `Route` create arrived") instead of polled document-state
   diffing. The `RUST_LOG=edit` mutation log was the debug-print form
   of exactly this signal; commits are its typed, reliable form.

---

## 14. Verification

- **The reconciliation suite** (successor to the convergence suite —
  ported structure, new properties; see the port playbook step 10):
  - *Server-order determinism*: any submission set, folded in rev order,
    yields the same state on server and every client — byte-identical.
  - *Optimistic convergence*: N simulated clients, random interleaving of
    edits, submissions, broadcasts, and acks ⇒ after quiescence every
    client's `optimistic == confirmed ==` server state.
  - *Prediction reconciliation*: a client with pending commits
    receiving foreign commits rebuilds `optimistic` equal to a
    from-scratch fold of (server log + pending) — the oracle for rule 2
    of §6.
  - *Idempotence*: re-delivered commits change nothing.
  - *Intra-commit ordering*: a commit writing one register twice
    resolves last-wins by seq.
  - *Undo round-trip* (§10) — the journal inverse restores the visible
    projection, with observability asserted.
  - *Rejection safety*: a rejected commit leaves confirmed state
    untouched and optimistic state rebuilt without it.
- **The mutation rule** (hard-won; applies to every property): break the
  rule a property claims to guard and watch it fail. A green suite you
  haven't watched fail proves nothing. Note the historical trap inverts
  here: "replicas fold one canonical linearization" was a test *bug* in
  the causal world and is the *design* in this one — so the interesting
  properties target the client's optimistic layer, which is where
  order-sensitivity now lives.
- **Semantic oracles**: at least one test per merge decision asserts
  *which value won*, not only that ends agree.
- `cargo-fuzz` on the server's decode-and-validate path (adversarial
  payloads from hostile clients).
- Integrated path: drive real gestures through the real tools
  (`script::Session`/`SimDriver`) against a real in-process server and
  assert the sequenced log, not just the resulting document.

## 15. Invariants checklist (enforce in code review)

- [ ] The fold is pure: no I/O, clocks, randomness, or re-executed logic.
- [ ] The same fold and `validate` code runs on server and client (shared
      crate); neither side carries a private variant.
- [ ] Every new op type reduces to LWW register / create / tombstone.
- [ ] Every global invariant is checked in `validate` (server ingress),
      not repaired after the fact.
- [ ] Every update op is invertible from its own fields.
- [ ] `Rev` is assigned in exactly one place (the server's single-writer
      task) and never reused.
- [ ] Clients never author write orders onto the wire — provisional
      orders stay client-internal.
- [ ] Element ids never reused; derived artifacts unreferenceable.
- [ ] New enum variants: stable serde name, decode-golden coverage,
      unknown-variant refusal path.
- [ ] Snapshots, indexes, caches: all rebuildable from the log.

---

## 16. What the pivot removed (for the record)

Deleted concepts, and what (if anything) replaced them:

| Removed | Replaced by |
|---|---|
| `Lamport`, `Clock`, clock-resume invariant | The server's rev counter (`INTEGER PRIMARY KEY`, single writer) |
| `ActorId`, `Stamp { lamport, actor }` | `Rev` in `WriteOrder { rev, seq }` |
| `CommitHash`, content addressing, bytes-immutable-after-mint | `Rev` as the commit identity; decode-forever replaces byte-canonicality |
| `parents`, the change DAG, heads, linearization, causal buffering | The log is linear; per-connection ordered delivery; contiguity asserts |
| Merge procedure (exchange/linearize/replay/repair), `Collision` machinery | Server-side `validate` at ingress; `suppressed` routes survive as a view derivation |
| `Swap { old, new }` on the wire (2026-08-13 follow-up) | Update ops carry only the new value; undo baselines captured into the client's journal at seal time |
| Convergence-under-arbitrary-delivery properties | The reconciliation suite (§14) |
| Offline editing / peer sync | Requires the server. A future local mode embeds the server in-process |

Everything else — the typed per-kind model, registers, `Live<T>`,
zero-value principle, the opcode/operand vocabularies (slimmed to plain
new-value payloads), the fold, `DocumentCache`, `Seq` and the total
`WriteOrder`, the derived draw order, `FracVal` determinism, the versioned
envelope — survives the pivot unchanged in shape, and most of it unchanged
in code.
