Skip to content

Commit 2dd989e

Browse files
docs(adr): 0006-simulation-semantics — isolated snapshots + manual merge (#94)
Closes #64. `verisimdb_simulation_branches` + `_deltas` tables exist but the semantics were undocumented. Without that, `simulate branch`, `simulate merge`, `simulate diff` would diverge across implementations. ADR-0006 pins: - **Branch creation**: `main` is implicit, new branches off any parent, frozen at branch-creation time (no auto-pickup of parent changes; rebase is OQ-1). - **Delta isolation**: snapshot isolation. Reads in branch see parent at branch start + own deltas. No sibling-branch leakage. - **Merge policy**: manual by default; `--strategy last-writer-wins` and `--strategy abandon-on-conflict` are opt-ins. **No CRDTs** — the data model is application-defined SQL rows, not CRDT primitives; faking CRDT semantics over arbitrary SQL is unsound. - **Conflict reporting**: structured `MergeConflict` per conflicting `(entity_id, table_name)` with branch_value, target_value, `target_provenance` (hash from provenance log, enabling audit of who last touched the target). - **Target-DB integration**: branch creation is sidecar-only; merge against `main` wraps in a target-DB transaction; merges are atomic (SAVEPOINT on sidecar, transaction on target). - **Lifecycle**: `active → merged | abandoned`; storage-layer CHECK from V-L2-J1 already enforces this. Three open questions called out for follow-up ADRs (rebase, cross-branch references, merge-as-provenance). Cross-references ADR-0004, V-L2-J1 (#43, merged), V-L2-P1 (#50), and the existing simulation DDL + CLI surface. Doc-only. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 598998a commit 2dd989e

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
= Architecture Decision Record: 0006-simulation-semantics
2+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
3+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> -->
4+
5+
# 6. Simulation: branch / delta / merge / conflict semantics
6+
7+
Date: 2026-05-14
8+
9+
## Status
10+
11+
Accepted
12+
13+
## Context
14+
15+
`verisimdb_simulation_branches` and `verisimdb_simulation_deltas`
16+
tables exist (`src/codegen/overlay.rs`) and `enable_simulation` is a
17+
manifest flag (per ADR-0004 the Simulation concern is canonical and
18+
Tier 2). What's missing is the *semantics*: when a user types
19+
`verisimiser simulate branch new-pricing`, what does the system
20+
promise to do?
21+
22+
Without that pinned, implementations of `simulate branch`,
23+
`simulate merge`, `simulate diff`, and the SQL the codegen layer
24+
emits for branch-aware reads will all diverge. This ADR is the
25+
binding reference.
26+
27+
## Decision
28+
29+
The simulation system is **isolated snapshots with explicit merge**.
30+
31+
### 6.1 Branch creation
32+
33+
* The root branch is `main` and corresponds to the target
34+
database's committed state. It is implicit; it has `branch_id =
35+
"main"`, `parent_branch = NULL`, and no deltas.
36+
* A new branch is created via
37+
`verisimiser simulate branch <name> [--from <parent>]`. Without
38+
`--from`, the parent is `main`. The branch is created in
39+
`status = 'active'` and inherits the parent's accumulated state
40+
*as of branch-creation time*.
41+
* The state inherited is the parent's *committed* state — i.e.
42+
the target DB rows plus the parent branch's resolved deltas.
43+
It is **frozen at branch-creation time** for the purposes of
44+
diff/merge; subsequent commits to the parent do not flow into
45+
the branch automatically. (Re-base is a future operation, not
46+
part of this ADR.)
47+
* Self-referencing FK on `parent_branch` (V-L2-J1, #43) is the
48+
storage-layer expression of this rule.
49+
50+
### 6.2 Delta isolation
51+
52+
* Each write within a branch produces a row in
53+
`verisimdb_simulation_deltas` with `(branch_id, entity_id,
54+
table_name, operation, delta_data)`.
55+
* **Reads within a branch** see the parent's state at branch
56+
creation, plus the branch's own deltas (applied in `created_at`
57+
order). Reads do **not** see deltas from sibling branches.
58+
* This is **snapshot isolation**: each branch sees a consistent
59+
point-in-time view of its parent at branch start. No phantom
60+
reads from siblings; no cross-branch interleavings.
61+
* Reads in `main` see the target database directly (no deltas
62+
table involved).
63+
* The Temporal concern continues to work *within* a branch —
64+
point-in-time queries scoped to that branch see the branch's
65+
version of history.
66+
67+
### 6.3 Merge policy
68+
69+
* Merge is **manual** by default:
70+
`verisimiser simulate merge <branch> --into <parent>` produces
71+
a report of every delta in `<branch>` that would conflict with
72+
state in `<parent>` (or `main`). The user must resolve each
73+
conflict explicitly (re-apply, drop, or modify) before the
74+
merge can complete.
75+
* A `--strategy last-writer-wins` flag opt-in is supported for
76+
bulk-resolution: when set, every delta in the merging branch
77+
wins over the parent automatically. This is unsafe by default
78+
and the CLI prints an explicit warning.
79+
* A `--strategy abandon-on-conflict` flag refuses the merge
80+
entirely if any conflict exists. Suited to "validate first,
81+
then merge later in a clean state" pipelines.
82+
* CRDTs are **not** offered. Reasoning: the data model is
83+
application-defined SQL rows, not CRDT primitives. Faking CRDT
84+
semantics over arbitrary SQL is unsound; pretending it works
85+
in the common case while breaking in edge cases is worse than
86+
manual resolution.
87+
88+
### 6.4 Conflict reporting
89+
90+
* A conflict is detected when, for the same `(entity_id,
91+
table_name)`, the merging branch's delta and the target's
92+
current state both modify a column with non-equal values.
93+
* The report is a `Vec<MergeConflict>` returned by the merge
94+
function and emitted to stdout (or `--json`):
95+
+
96+
[source,json]
97+
----
98+
{
99+
"entity_id": "post-42",
100+
"table_name": "posts",
101+
"branch_value": { "title": "Q3 Plan v2" },
102+
"target_value": { "title": "Q3 Plan v1.5" },
103+
"branch_op": "update",
104+
"target_provenance": "<hash from verisimdb_provenance_log>",
105+
"branch_delta_id": "<delta_id from verisimdb_simulation_deltas>"
106+
}
107+
----
108+
* `target_provenance` lets the user audit who last touched the
109+
target value before the merge attempt — combining Simulation
110+
with Provenance.
111+
112+
### 6.5 Integration with target-DB transactions
113+
114+
* `simulate branch` is a **sidecar-only** operation. The target
115+
database is not touched. Branch creation writes to
116+
`verisimdb_simulation_branches` only.
117+
* `simulate merge` against `main` *does* touch the target DB.
118+
The merge is wrapped in a single target-DB transaction; if the
119+
transaction rolls back, the corresponding deltas remain in the
120+
branch and the branch's `status` stays `active`. If it
121+
commits, the branch's `status` flips to `merged` and
122+
`merged_at` is set.
123+
* `simulate merge` against another branch (non-`main` parent)
124+
remains sidecar-only — moves deltas from child to parent
125+
table, no target write.
126+
* Failures must be **atomic per merge**: a partial merge of N
127+
conflict-free deltas plus a refusal on the N+1th must leave
128+
the system as if zero had been merged. The implementation
129+
wraps the sidecar writes in a SAVEPOINT and the target-DB
130+
writes in a transaction.
131+
132+
### 6.6 Lifecycle
133+
134+
* `status` transitions: `active → merged` (successful merge to
135+
parent), or `active → abandoned` (`simulate abandon`). The
136+
enum CHECK (V-L2-J1, #43) is the storage-layer expression.
137+
* Abandoned branches are kept by default (audit trail). The
138+
`[retention].simulation-days` field (a future addition to
139+
V-L2-P1) would gc them.
140+
141+
## Consequences
142+
143+
### Positive
144+
145+
* Implementers know what to build. `simulate branch`,
146+
`simulate merge`, `simulate diff`, `simulate abandon` have
147+
pinned semantics.
148+
* Snapshot isolation makes branch reads predictable. No
149+
cross-branch leakage; reproducible simulation runs.
150+
* Manual merge by default keeps the user in control of
151+
destructive operations on the target DB.
152+
* `target_provenance` in conflict reports glues Simulation to
153+
Provenance, exploiting the rest of the octad.
154+
155+
### Negative
156+
157+
* No CRDT means concurrent branches with overlapping writes
158+
always require human resolution. Acceptable for the "what
159+
if?" use case; would be painful for offline-first sync.
160+
* The "freeze at branch creation" rule means branches don't
161+
auto-pick-up parent changes. Rebase as a separate operation
162+
is out of scope for this ADR.
163+
* Merge against `main` touches the target DB, which means
164+
Tier 1's "never write to target" claim is *narrower* than
165+
it sounds: it holds for the Tier 1 concerns (provenance,
166+
lineage, temporal, access-control) but not for Simulation
167+
merges.
168+
169+
### Neutral
170+
171+
* The `verisimdb_simulation_deltas` table already exists with
172+
the right shape (`branch_id`, `entity_id`, `table_name`,
173+
`operation`, `delta_data`). No DDL change required.
174+
* The `verisimdb_simulation_branches.status` enum CHECK
175+
(V-L2-J1) already constrains the lifecycle states. No DDL
176+
change required.
177+
178+
## Open questions
179+
180+
* **OQ-1**: Should rebase be supported (port parent changes
181+
into a long-lived branch)? Suggested follow-up ADR.
182+
* **OQ-2**: Cross-branch references — can a branch delta cite
183+
an entity that only exists in a sibling branch? Currently
184+
no; pinned here to avoid the open-ended semantics of
185+
inter-branch dependencies.
186+
* **OQ-3**: Should merge produce a provenance entry in
187+
`verisimdb_provenance_log` recording the merge operation
188+
itself? Currently no — provenance is per-entity, not
189+
per-merge. A meta-provenance layer would be a separate ADR.
190+
191+
## Cross-references
192+
193+
* ADR-0004 — concerns octad (Simulation is the 8th concern).
194+
* V-L2-J1 (#43) — FK + status enum CHECK on simulation tables
195+
(already merged).
196+
* V-L2-P1 (#50) — retention; `simulation-days` field would be
197+
a future extension.
198+
* `src/codegen/overlay.rs::generate_simulation_table` — DDL.
199+
* `src/main.rs` — CLI surface for `verisimiser simulate …`
200+
(not yet implemented; awaits this ADR).

0 commit comments

Comments
 (0)