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

update

Field

Replace value at path

union_in_set

Set

Add elements

subtract_in_set

Set

Remove elements

union_in_map

Map

Add key-value pairs

subtract_in_map

Map

Remove keys

update_in_map

Map

Update existing key

insert_in_xarray

XArray

Insert at position

update_in_xarray

XArray

Update at position

remove_in_xarray

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_id explicitly. 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.