description: One correctness protocol for graph-visible writes, with synchronous recovery, complete read-set arbitration, writer-specific physical-effect adapters, and explicit control-plane exceptions.
A **graph-visible write** changes state resolved through a graph manifest snapshot:
- a node or edge table version;
- a registered or tombstoned table;
- accepted schema identity or schema-visible table metadata;
- a graph commit or graph head;
- any future logical marker that changes query or time-travel semantics.
For such an operation, the only visibility point is one successful `__manifest`
commit containing the entire graph delta. Per-table Lance commits before that point
are physical effects covered by recovery; they are not independently graph-visible.
### 1.2 Control operations
Two classes are not graph-visible commits:
1. Native graph-branch ref create/delete. Lance stores these refs outside the
dataset-version chain and creates no new `__manifest` version for either action.
2. Physical-only maintenance whose result is content-equivalent and is not selected
through a data-table pointer in `__manifest`, such as compacting `__manifest`
itself or reclaiming unreachable files.
Sections 7 and 8 define their control protocols. Branch **merge** and a data-table
optimize whose new version must be published are graph-visible writes and remain in
the main protocol.
### 1.3 What “unified” means
Unified means one set of safety obligations, one coordinator state machine, and one
closed registry of effect adapters. It does **not** mean:
- one storage trait method for every Lance operation;
- exactly one physical commit per table;
- pretending native refs are manifest rows;
- treating process-local queues as a distributed transaction manager;
- moving commit recovery out of the correctness path.
## 2. Protocol objects
The names below are conceptual. Implementations may choose different Rust names,
but they must preserve the represented information and transitions.
```rust
struct PreparedWrite {
operation_id: OperationId,
writer_kind: WriterKind,
target: BranchTarget,
base: BaseView,
read_set: ReadSet,
effects: Effects,
manifest_delta: ManifestDelta,
lineage_intent: Option<LineageIntent>,
recovery: RecoveryPlan,
}
```
### 2.1 `BaseView`
`BaseView` is the immutable state against which the operation was computed. It
contains at least:
- target manifest branch and its incarnation/freshness token;
- pinned manifest version;
- accepted schema identity;
- pinned table entries used by the operation;
- graph head when parentage, merge base, or branch semantics depend on it.
The base is captured only after the recovery barrier completes. A recovery pass may
advance the manifest or promote schema state, so a base captured before recovery is
not valid write input.
### 2.2 `ReadSet`
`ReadSet` is every authority value whose stability is required for the prepared
result to remain correct. It includes, as applicable:
- target branch incarnation;
- accepted schema identity;
- graph head;
- the table head for every table written;
- the table head for every table probed by uniqueness, referential-integrity,
cardinality, policy-independent structural validation, or merge classification;
- writer-specific authority, such as an enrolled stream's configuration and merge
generation.
A table belongs in the read set because its value affected the decision, not because
the writer happens to update it. Read-only dependencies are load-bearing.
The publisher must arbitrate every read-set member atomically with the manifest
commit. A fresh pre-check followed by an unconditional write is not a CAS. The
implementation must ensure that a concurrent change after the check contends on a
stable authority row or equivalent substrate token. If the current representation
cannot arbitrate a read-only dependency, that writer has not completed this RFC and
must not ship the outside-gate validation optimization.
### 2.3 `Effects`
`Effects` is an adapter-owned physical plan. Each effect declares:
- physical dataset and Lance branch/ref it targets;
- expected pre-state;
- whether it is staged, inline, ref-only, or zero-commit;
- the possible post-state shape: exact version, bounded range, or adapter-confirmed
version;
- whether rollback is safe;
- how recovery distinguishes no movement, partial movement, completed movement,
and an already-published result.
The generic coordinator never assumes `expected_version + 1`. That assumption is
valid for some mutation/load effects and false for branch merge, compaction, index
work, schema metadata changes, and no-op plans.
### 2.4 `ManifestDelta`
`ManifestDelta` is the complete logical result to publish. It contains table-version
journal entries, registrations/tombstones, mutable logical heads when present, schema
identity, and lineage rows as required by the operation. Its logical contents are
immutable for one prepared attempt. Physical version fields may be declared output
slots that the adapter binds to an allowed, confirmed effect result; binding such a
slot cannot widen or otherwise change the logical plan. A revalidation mismatch
discards the delta and restarts preparation.
### 2.5 `RecoveryPlan`
`RecoveryPlan` supplies the writer-specific classifier and compensation/roll-forward
rules. It must be serializable into a recovery sidecar before the first independently
durable physical effect. It is part of the commit protocol, not a best-effort
maintenance hint.
## 3. Normative invariants
1.**Recovery before base capture.** Every graph-visible writer runs and awaits the
recovery barrier before pinning `BaseView`.
2.**No durable effect before durable intent.** On an existing physical ref,
reclaimable uncommitted files may be staged first. A first-touch named-table
transaction is different: Lance writes its uncommitted files into the opened
branch tree, so Prepare retains the logical batch/predicate and pre-mints its
transaction identity; after revalidation the recovery sidecar becomes durable,
then the writer creates the target ref and stages those branch-local files under
the held gates. In every case the sidecar precedes the first independently
persistent physical effect, including a native table ref, data-table HEAD
advance, or native tag/index mutation needed by the graph-visible result.
3.**Complete read-set validation.** Physical effects may start only while the fresh
authority state equals the complete `ReadSet` used to prepare the operation.
4.**Recompute, do not patch.** On a pre-effect mismatch, discard the prepared plan
and rerun validation/classification. Never refresh expected versions beneath an
old plan.
5.**One graph visibility point.** A graph-visible write publishes one manifest CAS;
no subset of its table effects becomes graph-visible independently.
6.**Adapters own effect truth.** Every effect-producing writer uses a registered
adapter. There is no generic fallback that guesses version movement or recovery
safety.
7.**Queues are local optimization.** Ordered process-local gates prevent avoidable
in-process races and deadlocks. Cross-process correctness comes from Lance
conflicts, manifest arbitration, and recovery.
8.**Recovery safety is synchronous.** Background sweeping is permitted, but a later
overlapping writer waits for, performs, or fails on unresolved recovery before it
can advance state.
9.**Derived work follows visibility.** Expensive index reconciliation, cache warming,
and orphan reclaim may run asynchronously only when logical correctness does not
depend on their completion.
These refine, and do not weaken, invariants 2, 3, 4, 5, 7, 9, 13, and 15 in
[`docs/dev/invariants.md`](../dev/invariants.md).
## 4. The graph-write state machine
### 4.1 Stage A — recovery barrier
Before accepting a base, the coordinator discovers pending recovery intents that can
overlap this operation. It must reach one of three outcomes:
1. all relevant intents are fully resolved;
2. each remaining intent is proven already satisfied and can be finalized safely;
3. the write fails with a typed recovery-required or live-writer-contention error.
“Spawn recovery and continue” is not an outcome. Listing sidecars may run concurrently
with non-authoritative I/O, but preparation cannot accept its base until recovery has
completed and the post-recovery schema/manifest state is available.
Recovery must not destructively act on a sidecar that may belong to a live writer
without ownership/fencing proof. It waits, returns contention, or uses a protocol that
proves the prior owner cannot continue.
Recovery classifiers use exact effect identity or confirmed post-state. A numeric
test such as `manifest_version >= observed_lance_head` is sufficient only when the
adapter independently proves lineage containment; version ordering alone is not that
proof. Exact identity cuts both ways: a roll-forward that loses its manifest CAS to
a concurrent writer which already published this intent's exact goal state is
**convergence, not failure** — the barrier records the audit outcome and removes the
intent rather than failing the write (preserving the concurrent-advance convergence
behavior fixed in #296). "Exact" forbids adopting an unrelated newer version; it does
not forbid recognizing one's own goal already achieved.
The barrier is a deliberate availability trade, stated plainly: an unresolvable
overlapping recovery intent — including a live foreign writer's sidecar that cannot
be fenced — fails every overlapping write with a typed error until resolved. Safety
outranks availability here by design; operators observe the condition through the
typed error and recovery telemetry rather than through silently degraded writes.
### 4.2 Stage B — prepare
Prepare runs without writer gates. It may:
- capture `BaseView`;
- evaluate the complete constraint set;
- classify a merge;
- compute embeddings or other deterministic payloads;
- build staged Lance transactions and reclaimable uncommitted objects for targets
whose physical refs already exist;
- for a first-touch named-table target, retain the complete logical stage input and
pre-mint its recovery transaction identity without creating the ref;
- construct the complete `ReadSet`, `Effects`, `ManifestDelta`, and `RecoveryPlan`.
Prepare must not advance a Lance HEAD or the graph's native branch refs. Staged files
that are not referenced by a committed Lance manifest are permitted and are
reclaimable if the attempt restarts.
After Stage E arms recovery, a first-touch adapter may create its declared target ref
under the held gates, stage branch-local files on that ref, bind the resulting Lance
transaction to the pre-minted identity, and only then advance HEAD. The ref is itself
an independently durable effect covered by the sidecar; recovery must classify both
sidecar-before-ref and ref-before-HEAD crash states.
Every validator registers its actual committed-state probes in `ReadSet`. Key fences
are an additional same-key conflict signal; they do not replace read-set arbitration
for non-key uniqueness, RI, cardinality, or merge-target stability.
### 4.3 Stage C — acquire ordered gates
The prepared effects declare their gate keys. One attempt acquires the complete set in
this total order:
1. durable global operation claims (migration, retention, or future claims) by
deterministic claim key;
2. graph/schema-control key, if required;
3. target branch-control key, if required;
4.`(physical_branch, table_key)` keys in deterministic sorted order.
The gates are held through revalidation, recovery arming, physical effects, and the
manifest visibility decision. They may be released after a successful manifest CAS or
after a failed post-effect attempt has safely left its sidecar for recovery.
Acquiring a global claim is coordination, not a graph effect for §3's sidecar-order
rule: the claim record must itself contain an owner/fencing token and an explicit
crash-release/takeover contract. No data HEAD, tag, index, or logical authority may
move merely because the claim was acquired.
The merge-exclusive mutex may protect a coordinator swap, but it is not a semantic
cross-process lock and must not substitute for a target read set.
### 4.4 Stage D — revalidate or restart
With all gates held, the coordinator loads fresh authority state and compares every
member of `ReadSet`.
- If all members match, the attempt may arm recovery.
- If any member differs, no physical effect may run. The attempt releases its gates,
discards staged state, and restarts from Prepare.
- A strict API may return a typed conflict instead of retrying, but it may not publish
the stale plan.
Retries are bounded and observable. Retrying a merge means recomputing the merge base
and reclassifying against the new target. Retrying a validation-sensitive mutation
means rerunning the validators, including probes of tables the mutation does not
write.
### 4.5 Stage E — arm recovery
After successful revalidation, write the recovery sidecar durably before the first
independently durable physical effect. An effect-free or authority-first workflow
described in §4.9 may omit the sidecar. Otherwise the sidecar contains at least:
- operation id, writer kind, actor, and target branch/incarnation;
- pinned schema identity and complete read set;
- every physical target and expected pre-state;
- adapter recovery strategy;
- intended manifest delta and lineage intent, or a durable reference to them;
- confirmed post-state once the adapter reaches its all-effects-complete boundary.
A multi-step adapter first records its pre-state plan. After all its physical effects
finish, it durably records the exact confirmed post-state before manifest publish.
Until that confirmation exists, recovery treats the effect set as possibly partial.
### 4.6 Stage F — apply effects
The adapter applies its declared physical effects. A failure after recovery is armed
leaves the sidecar intact. The request must not delete it, silently adopt live HEAD, or
start a fresh plan around it.
The adapter returns exact achieved state to bind the physical output slots declared by
`ManifestDelta`. If achieved state differs from the prepared effect envelope, the
operation fails into recovery rather than widening its plan in place.
### 4.7 Stage G — manifest CAS
A graph-visible operation performs exactly one `__manifest` CAS carrying its complete
logical delta and lineage when the plan has `LineageIntent`. Metadata-only plans carry
their explicit authority/operation rows without manufacturing graph lineage. On every
CAS attempt, the commit authority re-reads and arbitrates the complete `ReadSet`.
The following cases are distinct:
- **Conflict before any physical effect:** safe bounded restart from Prepare.
- **Conflict after physical effects:** recovery case. Keep the sidecar; do not simply
re-stage or point the manifest at whatever HEAD is now live.
- **CAS success:** the graph commit is visible atomically.
When durable table heads land under RFC-024, tombstoning a table must update its mutable
head to an explicit deleted state in this same CAS. A stale live head plus an immutable
tombstone history is not a valid O(tables) current-state representation.
### 4.8 Stage H — finalize
After CAS success:
- delete the sidecar best-effort when the workflow has one;
- refresh/invalidate process-local views;
- enqueue derived index reconciliation and orphan reclaim;
- record recovery/audit completion when applicable.
Sidecar deletion failure does not turn a durable successful graph commit into a user
error. The next recovery barrier proves the exact intent satisfied and removes the
artifact.
### 4.9 Authority-first control workflows
Some metadata workflows have no independently durable physical effect before their
manifest CAS. They may use an **authority-first** subtype:
1. run the recovery barrier, prepare, acquire gates/claims, and revalidate exactly as
above;
2. publish the metadata transition as the first durable effect;
3. perform only idempotent work whose desired target is fully encoded by that
transition and whose interruption cannot expose incorrect graph data.
No generic sidecar is required before step 2 because there is no pre-authority effect
to recover. The authority row itself is the durable recovery cursor. Checkpoint
deletion, a GC-boundary publish, and `OPEN -> DRAINING` stream intent are candidate
examples; each follow-up RFC must prove its post-CAS work is convergent and safe.
A long control workflow is not one giant RFC-022 attempt. Each lifecycle transition,
fold, schema/branch operation, and resume transition is a separate prepared write or
native-ref control step with its own read set and visibility point. If a later phase
needs a non-idempotent or independently visible effect not fully described by the
authority row, that phase uses a normal sidecar before the effect.
## 5. Crash contract
| Crash point | Required result |
|---|---|
| Before sidecar | No independently durable effect occurred; uncommitted objects are reclaimable. |
| Sidecar durable, no effect | Recovery aborts/finalizes the empty intent. |
| Some effects applied, not confirmed | The adapter rolls back, completes, or refuses safely according to its declared strategy. |
| All effects confirmed, before manifest CAS | Recovery rolls forward the exact confirmed manifest delta, or applies the adapter's explicit all-or-nothing rule. |
| Manifest CAS succeeded, sidecar remains | Recovery proves the exact intent visible, audits it, and removes the sidecar. |
Rollback is not assumed safe. Lance `Restore`, schema-file promotion, native refs, and
content-replacing operations have different concurrency properties; each adapter must
state which recovery direction is legal and under what fencing.
## 6. Writer-effect adapters
The adapter registry is closed by default: adding a graph-visible writer requires a
new adapter or an explicit use of an existing adapter. Code review and tests must be
able to enumerate every adapter and every entry point that invokes it.
### 6.1 Mutation and load
- Construct one staged effect per touched table where the Lance API permits it.
Existing-table effects may stage in Prepare; first-touch named-table effects use
the sidecar → target ref → branch-local stage ordering above.
- Put every uniqueness, RI, and cardinality probe in `ReadSet`.
- Revalidate or restart when a probed-but-untouched table changes.
- Preserve strict replacement semantics for overwrite/delete.
- Treat key-conflict fencing and strict keyed Append semantics as RFC-023 concerns;
no fence is credited as protection until that RFC's rollout gates pass.