ADR-0008 — Change-journal sync with conflicted-copy resolution

V1 Freeze (2026-06-12): Accepted. Blocker-3 resolution: the change journal is the source of truth, written transactionally at commit by File & Metadata — it is not a projection of NodeChanged events. This removes the at-least-once ordering hazard the pre-freeze review identified. Delta transfer in V1 is whole-object (sub-file chunk-delta is deferred with ADR-0017).

Context

Synchronization is BitVault’s headline competency and its highest-severity risk (R1). Multi-device, offline-tolerant sync creates concurrent divergent histories. The cardinal sin is silent data loss — last-writer-wins clobbering a user’s work. We must choose a model that is correct, implementable by a solo developer, and demonstrable. Full CRDT/OT co-editing is a non-goal (NG1), so we do not need character-level merge.

The pre-freeze review (review §3.3) found a fatal ambiguity: the journal was described both as “the source of truth” (this ADR, ADR-0024) and as “the projection of NodeChanged events” fed from an at-least-once, unordered bus (data model I4, roadmap P3). A monotonic, gap-free total order cannot be safely derived from an at-least-once unordered consumer. The sync engine’s correctness (ADR-0022 leans entirely on the journal providing a reliable total order), so this had to be resolved before coding.

Decision

A change-journal + content-addressed identity + conflicted-copy model:

  1. Per-tenant monotonic change journal — source of truth, written at commit. Every namespace mutation appends a CHANGE(seq, node, op, version, actor) whose tenant-monotonic seq is assigned inside the File & Metadata commit transaction — the same transaction that inserts the VERSION row and the OUTBOX row (the commit protocol, 06 §4). Because seq is allocated under the commit’s transactional ordering, the journal is gap-free and totally ordered per tenant by construction, with no dependence on event delivery. File & Metadata owns and writes the journal (05); Sync reads it and owns only device_cursors and conflict_records.
  2. Cursor-based delta pull (authoritative) + lossy notify (ADR-0024). Each device stores sync_cursor; pulling changes is seq > cursor ORDER BY seq against the journal. A lossy “namespace advanced” notification merely prompts a pull; losing it costs latency, never correctness.
  3. Read-your-writes for sync. Because seq is assigned at commit (not by an async projector), a device that commits a change sees it in the very next delta pull — there is no projection lag between the namespace and the journal.
  4. Content identity by hash (ADR-0016). Unchanged content is never re-transferred (per-tenant dedup, ADR-0018). A “change” with an identical hash is a no-op move/touch. V1 transfers whole objects; sub-file delta is deferred (ADR-0017).
  5. Optimistic concurrency on commit. A push carries the baseVersion it edited. If baseVersion is current → fast-forward commit. If stale (a concurrent change landed) → create a conflicted copy (e.g. report (conflict, alice, 2026-06-11).docx) and keep both versions. Never overwrite.
  6. Version history as the safety net. Every change is a recoverable VERSION (FR B4), so even an unexpected outcome is reversible.

CRDTs are explicitly out for whole-file sync (overkill, NG1); reserved only if real-time co-editing ever becomes a goal.

Consequences

Positive

Negative / costs

Alternatives considered

Verification

The conflict harness (P2 acceptance, 09): two simulated offline devices edit the same file from the same base → assert a conflicted copy exists, both versions are retrievable, and the journal/cursors converge. This is the single most important test in the project.