Date: 2026-05-14
verisimdb_simulation_branches and verisimdb_simulation_deltas
tables exist (src/codegen/overlay.rs) and enable_simulation is a
manifest flag (per ADR-0004 the Simulation concern is canonical and
Tier 2). What’s missing is the semantics: when a user types
verisimiser simulate branch new-pricing, what does the system
promise to do?
Without that pinned, implementations of simulate branch,
simulate merge, simulate diff, and the SQL the codegen layer
emits for branch-aware reads will all diverge. This ADR is the
binding reference.
The simulation system is isolated snapshots with explicit merge.
-
The root branch is
mainand corresponds to the target database’s committed state. It is implicit; it hasbranch_id = "main",parent_branch = NULL, and no deltas. -
A new branch is created via
verisimiser simulate branch <name> [--from <parent>]. Without--from, the parent ismain. The branch is created instatus = 'active'and inherits the parent’s accumulated state as of branch-creation time. -
The state inherited is the parent’s committed state — i.e. the target DB rows plus the parent branch’s resolved deltas. It is frozen at branch-creation time for the purposes of diff/merge; subsequent commits to the parent do not flow into the branch automatically. (Re-base is a future operation, not part of this ADR.)
-
Self-referencing FK on
parent_branch(V-L2-J1, #43) is the storage-layer expression of this rule.
-
Each write within a branch produces a row in
verisimdb_simulation_deltaswith(branch_id, entity_id, table_name, operation, delta_data). -
Reads within a branch see the parent’s state at branch creation, plus the branch’s own deltas (applied in
created_atorder). Reads do not see deltas from sibling branches. -
This is snapshot isolation: each branch sees a consistent point-in-time view of its parent at branch start. No phantom reads from siblings; no cross-branch interleavings.
-
Reads in
mainsee the target database directly (no deltas table involved). -
The Temporal concern continues to work within a branch — point-in-time queries scoped to that branch see the branch’s version of history.
-
Merge is manual by default:
verisimiser simulate merge <branch> --into <parent>produces a report of every delta in<branch>that would conflict with state in<parent>(ormain). The user must resolve each conflict explicitly (re-apply, drop, or modify) before the merge can complete. -
A
--strategy last-writer-winsflag opt-in is supported for bulk-resolution: when set, every delta in the merging branch wins over the parent automatically. This is unsafe by default and the CLI prints an explicit warning. -
A
--strategy abandon-on-conflictflag refuses the merge entirely if any conflict exists. Suited to "validate first, then merge later in a clean state" pipelines. -
CRDTs are not offered. Reasoning: the data model is application-defined SQL rows, not CRDT primitives. Faking CRDT semantics over arbitrary SQL is unsound; pretending it works in the common case while breaking in edge cases is worse than manual resolution.
-
A conflict is detected when, for the same
(entity_id, table_name), the merging branch’s delta and the target’s current state both modify a column with non-equal values. -
The report is a
Vec<MergeConflict>returned by the merge function and emitted to stdout (or--json):{ "entity_id": "post-42", "table_name": "posts", "branch_value": { "title": "Q3 Plan v2" }, "target_value": { "title": "Q3 Plan v1.5" }, "branch_op": "update", "target_provenance": "<hash from verisimdb_provenance_log>", "branch_delta_id": "<delta_id from verisimdb_simulation_deltas>" } -
target_provenancelets the user audit who last touched the target value before the merge attempt — combining Simulation with Provenance.
-
simulate branchis a sidecar-only operation. The target database is not touched. Branch creation writes toverisimdb_simulation_branchesonly. -
simulate mergeagainstmaindoes touch the target DB. The merge is wrapped in a single target-DB transaction; if the transaction rolls back, the corresponding deltas remain in the branch and the branch’sstatusstaysactive. If it commits, the branch’sstatusflips tomergedandmerged_atis set. -
simulate mergeagainst another branch (non-mainparent) remains sidecar-only — moves deltas from child to parent table, no target write. -
Failures must be atomic per merge: a partial merge of N conflict-free deltas plus a refusal on the N+1th must leave the system as if zero had been merged. The implementation wraps the sidecar writes in a SAVEPOINT and the target-DB writes in a transaction.
-
statustransitions:active → merged(successful merge to parent), oractive → abandoned(simulate abandon). The enum CHECK (V-L2-J1, #43) is the storage-layer expression. -
Abandoned branches are kept by default (audit trail). The
[retention].simulation-daysfield (a future addition to V-L2-P1) would gc them.
-
Implementers know what to build.
simulate branch,simulate merge,simulate diff,simulate abandonhave pinned semantics. -
Snapshot isolation makes branch reads predictable. No cross-branch leakage; reproducible simulation runs.
-
Manual merge by default keeps the user in control of destructive operations on the target DB.
-
target_provenancein conflict reports glues Simulation to Provenance, exploiting the rest of the octad.
-
No CRDT means concurrent branches with overlapping writes always require human resolution. Acceptable for the "what if?" use case; would be painful for offline-first sync.
-
The "freeze at branch creation" rule means branches don’t auto-pick-up parent changes. Rebase as a separate operation is out of scope for this ADR.
-
Merge against
maintouches the target DB, which means Tier 1’s "never write to target" claim is narrower than it sounds: it holds for the Tier 1 concerns (provenance, lineage, temporal, access-control) but not for Simulation merges.
-
OQ-1: Should rebase be supported (port parent changes into a long-lived branch)? Suggested follow-up ADR.
-
OQ-2: Cross-branch references — can a branch delta cite an entity that only exists in a sibling branch? Currently no; pinned here to avoid the open-ended semantics of inter-branch dependencies.
-
OQ-3: Should merge produce a provenance entry in
verisimdb_provenance_logrecording the merge operation itself? Currently no — provenance is per-entity, not per-merge. A meta-provenance layer would be a separate ADR.
-
ADR-0004 — concerns octad (Simulation is the 8th concern).
-
V-L2-J1 (#43) — FK + status enum CHECK on simulation tables (already merged).
-
V-L2-P1 (#50) — retention;
simulation-daysfield would be a future extension. -
src/codegen/overlay.rs::generate_simulation_table— DDL. -
src/main.rs— CLI surface forverisimiser simulate …(not yet implemented; awaits this ADR).