Commit Engine¶
The Commit Engine provides transactional persistence with history tracking.
When to use: Use CommitDatabase for versioned persistence with history, concurrent
streams, and sync. Every change creates a commit, enabling undo/redo and concurrent editing.
Modes of Use¶
How you exercise the commit DAG determines which guarantees you can rely on and where validation belongs. Four user-facing modes — the first three are single-author, so the application arbitrates every change or head merge and the dual-layer contract is not load-bearing. The fourth is where the contract on the next page becomes the centre of gravity.
Time travel (read-only)¶
Reconstruct any past state from a commitId. No writes, so only the
read-side structural guarantees apply: determinism, immutability,
content-addressing.
Single-user undo / redo¶
Step back along the chain, diverge, redo. Intent: revise history — correct or replay past decisions on a single author’s line. All structural guarantees apply, plus tombstone semantics.
Single-user exploration¶
Diverge the DAG and keep parallel heads alive, merging heads on your own schedule. Intent: explore alternatives in parallel — same machinery as undo/redo, but you maintain multiple heads concurrently instead of replaying one. All structural guarantees apply, plus multi-head machinery.
Automated multi-user¶
Concurrent commits from multiple authors converge without human review. The engine guarantees structural soundness only; semantic integrity (uniqueness, referential integrity, cross-field invariants) is your problem. This is where the Dual-Layer Contract becomes load-bearing.
Note
Three regimes of multi-author work — only one is what the engine provides:
Collaboration — humans reconcile intentions: conflicts are identified, surfaced, resolved (the git-merge / review model).
Cooperation — disjoint contributions assemble without conflict by construction.
Mechanical convergence — the engine linearises streams deterministically with no notion of “conflict”: clashing intentions are silently reconciled by structural rules. Structurally sound, semantically untrusted.
dsviper offers mechanical convergence. Cooperation is achievable by structuring work along disjoint paths (Why Paths Matter). Collaboration requires an explicit application layer on top — that is what the Dual-Layer Contract formalises.
Opening a CommitDatabase¶
>>> db = CommitDatabase.open("model.cdb")
To create a new database with embedded definitions, use:
python3 tools/dsm_util.py create_commit_database model.dsm model.cdb
Reading State¶
A freshly created database has no commits — first_commit_id() and
last_commit_id() return None:
>>> db.first_commit_id() is None
True
>>> db.last_commit_id() is None
True
>>> db.head_commit_ids()
set()
The initial_state() method always works and returns the empty state:
>>> initial = db.initial_state()
>>> len(initial.attachment_getting().keys(TUTO_A_USER_LOGIN))
0
AttachmentGetting Interface¶
Read attachments via attachment_getting():
>>> getting = state.attachment_getting()
>>> doc = getting.get(attachment, key)
>>> doc
Optional({...})
>>> keys = getting.keys(attachment)
Mutations¶
Create a mutable state and apply changes:
>>> mutable_state = CommitMutableState(db.state(db.last_commit_id()))
>>> mutating = mutable_state.attachment_mutating()
>>> mutating.set(attachment, key, document)
>>> mutating.update(attachment, key, path, new_value)
Note: CommitDatabase tracks history via mutations from CommitMutableState.
Committing¶
commit_mutations() returns the new commit id — capture it explicitly to
chain further mutations or read the resulting state:
>>> commit_id = db.commit_mutations("Commit message", mutable_state)
Complete Example¶
Add an Alice document and read it back:
>>> key = TUTO_A_USER_LOGIN.create_key()
>>> login = TUTO_A_USER_LOGIN.create_document()
>>> login.nickname = "alice"
>>> login.password = "secret"
>>> mutable = CommitMutableState(db.initial_state())
>>> mutable.attachment_mutating().set(TUTO_A_USER_LOGIN, key, login)
>>> commit_id = db.commit_mutations("Add Alice", mutable)
>>> state = db.state(commit_id)
>>> state.attachment_getting().get(TUTO_A_USER_LOGIN, key)
Optional({nickname='alice', password='secret'})
Path-Based Mutators¶
Instead of replacing entire documents with set(), path-based mutators use
Paths to target specific locations. This enables path-based merging when multiple
users edit concurrently.
Mutator |
Target |
Operation |
|---|---|---|
|
Field |
Replace value at path |
|
Set |
Add elements |
|
Set |
Remove elements |
|
Map |
Add key-value pairs |
|
Map |
Remove keys |
|
Map |
Update existing key |
|
XArray |
Insert at position |
|
XArray |
Update at position |
|
XArray |
Remove at position |
Field Update¶
>>> mutating.update(TUTO_A_USER_LOGIN, key, TUTO_P_LOGIN_NICKNAME, "alice_updated")
Why Paths Matter¶
When two users edit different fields simultaneously:
User A: update(attachment, key, path_to_name, "Alice")
User B: update(attachment, key, path_to_email, "bob@example.com")
After convergence: Both updates apply (disjoint paths)
With set(), one user’s changes would overwrite the other’s.
Commit History¶
Inspect commit metadata:
>>> header = db.commit_header(commit_id)
>>> header.label()
'Add Alice'
>>> header.parent_commit_id() == ValueCommitId()
True
The first commit’s parent is the zero ValueCommitId (no ancestor).
Navigate history by passing the explicit ids you captured:
>>> state1 = db.state(first_commit_id)
>>> state2 = db.state(latest_commit_id)
Embedded Definitions¶
CommitDatabase stores its definitions:
>>> defs = db.definitions()
>>> sorted(str(t) for t in defs.types())
['Tuto::Account', 'Tuto::Identity', 'Tuto::Login', 'Tuto::Status', 'Tuto::Texture', 'Tuto::Thumbnail', 'Tuto::User']
Calling defs.inject() makes TUTO_A_USER_LOGIN, TUTO_S_LOGIN, etc.
available as constants in the calling namespace.
Safe Usage¶
A checklist for the operational gotchas. None of this is enforced by the engine — it’s on the application.
Identify your mode first. The four Modes of Use carry different burdens. Only automated multi-user makes the Dual-Layer Contract load-bearing; the other three are safe to use without it.
Capture
commit_idexplicitly.commit_mutations()returns the new id; there is no implicit current commit to auto-advance. Chain further mutations and reads from the captured value.Prefer path-based mutators over
set()for fields edited concurrently.set()replaces the whole document, so disjoint edits collide.update,union_in_set,update_in_map, etc. converge cleanly on disjoint paths — see Why Paths Matter.Do not assume a mutation landed. After convergence, mutations targeting non-existent documents or unresolved paths are silently dropped. If the outcome matters, read the state back and check.
Validate on read, not on write, when the contract applies. Engine output is structurally sound but semantically untrusted (why) — enforce uniqueness, referential integrity, and cross-field invariants when you consume the state, not when you build the mutations.