Commit Application Model

The Commit Application Model is the architectural pattern shared by every application built on the Commit Engine. It defines how an application composes a CommitStore (provided by the Viper runtime) with its own domain state, dispatch surface, and platform notifications, so that all state changes flow through the commit DAG with built-in undo/redo, multi-author convergence, and audit trail.

The walkthroughs that follow this page — cdbe.py, ge-py, ge-qml, web-cdbe — are concrete instances of this single Model, in different languages and on different presentation tiers. The Model is what they have in common.

dbe.py is not an instance of this model. It is a plain CRUD inspector for the non-versioned Database backend — it does not use a CommitStore and does not flow through a commit DAG. Outside the pattern.

The Application Context

The pattern is centred on an Application Context — a singleton (or per-document object) that owns the CommitStore and exposes the application’s surface to the UI and to scripts.

Composition, not subclassing. The Context owns a CommitStore provided by the runtime; it does not extend it.

The Context is responsible for four things:

  1. Owning the CommitStore (which itself holds the CommitDatabase and the current CommitState).

  2. Holding application-level domain state — the keys, selections, or session-level data not committed to the DAG.

  3. Dispatching user actions through the store, so every mutation becomes a commit.

  4. Notifying the UI through a platform-agnostic interface, so the same Context can drive Qt Widgets, QML, AppKit, or a web view without changing the core code.

In C++, the Context additionally holds the generated function pools (see below). In Python, function pools are unnecessary and the Context holds direct references to the business-logic modules.

Two language profiles

The same Model takes two slightly different shapes depending on the language of the business logic. The difference is whether function pools are needed to cross a language boundary.

C++ profile — six layers, with function pools

When business logic is written in C++ and needs to be reachable from Python scripting, the Application Context exposes generated function pools that bridge the two languages.

┌─────────────────────────────────────────────────────────────┐
│  1. UI Layer (AppKit / Qt Widgets / QML / …)                │
│     ViewControllers + components + notifications            │
├─────────────────────────────────────────────────────────────┤
│  2. Application Context (CommitStore Layer)                 │
│     Singleton: store + state + notifier + pools             │
├─────────────────────────────────────────────────────────────┤
│  3. Bridge Layer (hand-written)                             │
│     Pool bridges (FunctionPool + AttachmentFunctionPool)    │
│     dispatch generated pool calls into business logic       │
├─────────────────────────────────────────────────────────────┤
│  4. Business Logic Layer (hand-written, in C++)             │
│     Domain functions: Vertex, Edge, Graph, …                │
├─────────────────────────────────────────────────────────────┤
│  5. Generated Infrastructure (Kibo)                         │
│     Data, Database, ValueEncoder, FunctionPools             │
├─────────────────────────────────────────────────────────────┤
│  6. Viper Runtime (C++)                                     │
│     Type, Value, Commit, Database, RPC                      │
└─────────────────────────────────────────────────────────────┘

Function pools (and their bridges) exist for two reasons:

  • They expose C++ business logic to the Viper runtime through a typed surface — typed methods registered with Viper’s type system, whose implementations are routed to hand-written C++ code through the generated pool bridges.

  • They give the dispatch layer a uniform handle on actions — store->dispatch("label", pool.function, args...) — without knowing where the implementation lives.

Once a pool is registered with Viper, its Python availability is free. Viper’s “Metadata Everywhere” principle means every value and every function carries its type metadata at runtime; dsviper (the hand-maintained Python wrapper of Viper, shipped as a wheel on PyPI) introspects registered pools and calls them with automatic marshalling between Python values and Viper values. No per-pool Python binding is generated, none is written by hand — the same mechanism that lets dsviper expose any Viper type to Python exposes the pools too.

How pools reach Python — Context injection + Viper introspection

The application embeds CPython, imports dsviper and a thin app extension module that publishes the Python wrapper of the Context, and injects the Context singleton as a Python global (typically ctx). From there, every pool held by the Context is callable directly: ctx.modelGraph.new_vertex(...) reaches the hand-written C++ implementation through the generated pool bridge, with Python ↔ Viper marshalling handled automatically by dsviper from the runtime metadata. No per-pool binding code is written or generated.

This mirrors what PythonEditorModel(..., namespace_vars={...}) does for the Python profile. The two profiles converge on the same scripting surface — ctx.dispatch("label", ...) against the store — and differ only in whether the business functions are Python (Python profile) or C++ fronted by typed pools (C++ profile).

A real Application Context, abridged from the Graph Editor C++ reference (GE::Context):

namespace GE {

class Context final {
public:
    static std::shared_ptr<Context> Instance();

    // Database lifecycle
    void use(std::shared_ptr<Viper::CommitDatabase> const & database);
    void close();
    void load();
    void reset();

    // Domain actions
    void newGraph();
    void useNewGraph(std::string const & label);
    void dispatchAction(std::string const & label,
                        std::shared_ptr<ContextAction> const & action);

    // Composed runtime
    std::shared_ptr<Viper::CommitStore> store;

    // Generated function pools
    std::shared_ptr<Viper::FunctionPool>            tools;
    std::shared_ptr<Viper::AttachmentFunctionPool>  modelGraph;
    std::shared_ptr<Viper::AttachmentFunctionPool>  modelSelection;
    std::shared_ptr<Viper::AttachmentFunctionPool>  modelIntegrity;
    std::shared_ptr<Viper::AttachmentFunctionPool>  attachments;

    // Domain state (not committed to the DAG)
    GE::Graph::GraphKey graphKey;

private:
    Context();
};

} // namespace GE

Note the four ingredients of the pattern in plain sight: the Instance() singleton, the store (composed, not inherited), the typed function pools, and the domain state (graphKey). The notification adapter is held by the store itself.

Python profile — five layers, no function pools

When business logic is already in Python, there is no language boundary to cross. Function pools become redundant: the dispatch lambda calls the model functions directly.

┌─────────────────────────────────────────────────────────────┐
│  1. UI Layer (Qt Widgets, QML, HTML/CSS, …)                 │
│     Framework-specific presentation                         │
├─────────────────────────────────────────────────────────────┤
│  2. Application Context (CommitStore facade)                │
│     Owns the store, exposes domain state and dispatch       │
├─────────────────────────────────────────────────────────────┤
│  3. Business Logic (hand-written Python)                    │
│     model/*.py — direct calls, no pool indirection          │
├─────────────────────────────────────────────────────────────┤
│  4. Generated Data (Kibo output)                            │
│     Data classes, attachments, definitions                  │
├─────────────────────────────────────────────────────────────┤
│  5. dsviper Runtime (C++ binding)                           │
│     CommitStore, CommitDatabase, CommitMutableState, Value  │
└─────────────────────────────────────────────────────────────┘

A real Python Application Context, abridged from ge-py (model/context.py):

class Context:
    @classmethod
    def instance(cls) -> "Context":
        if not hasattr(cls, "_instance"):
            setattr(cls, "_instance", cls())
        return getattr(cls, "_instance")

    def __init__(self):
        self.store = CommitStore()
        self.graph_key = Graph_GraphKey.create()

    # Database lifecycle
    def use(self, database: CommitDatabase): ...
    def close(self): ...
    def load(self): ...
    def reset(self): ...

    # Domain actions
    def new_graph(self): ...
    def use_new_graph(self, label: str): ...

    # Dispatch — direct, no pool indirection
    def dispatch(self, label: str, func, *args):
        self.store.dispatch(label, func, *args)

Same three ingredients as the C++ Context: singleton, composed store, domain state (graph_key). The function-pool block is absent — Python business logic in model/*.py is called directly from the dispatch lambda.

This profile applies to all four Commit Applications shipped or walked through in the DevKit:

Application

Domain

UI tier

cdbe.py

Generic (no domain)

Qt Widgets (PySide6)

ge-py

Graph

Qt Widgets (PySide6)

ge-qml

Graph

Qt Quick / QML (PySide6)

web-cdbe

Generic (no domain)

Server-rendered HTML/CSS

cdbe.py and web-cdbe are generic — they have no DSM model of their own and no Kibo-generated infrastructure. They open any CommitDatabase through the dynamic introspection API, which means their layer 3 (business logic) and layer 4 (generated data) collapse to a thin glue layer over the runtime. The pattern still applies, but with three layers in practice instead of five.

ge-py and ge-qml are domain-specific — they have a DSM model (Graph, Vertex, Edge), Kibo-generated typed accessors, and hand-written Python business logic. They are the full five-layer instances of the Model.

The dispatch pattern

All state mutations go through dispatch. The store creates a mutable snapshot, runs the function, persists the resulting commit, and notifies observers — atomically.

In C++, dispatch takes a pool function:

store->dispatch("Create Vertex",
    [&](auto const & attachmentMutating) {
        Model::Vertex::add(attachmentMutating, graphKey, value, position);
    });

In Python, dispatch takes a lambda calling business logic directly (no pool indirection):

store.dispatch("Create Vertex",
    lambda m: model.vertex.add(m, graph_key, value, position))

After the lambda returns, the store has:

  • Created an immutable CommitState snapshot from the mutations.

  • Persisted the commit into the DAG.

  • Notified observers via the notification contract (below).

The notification contract

The store communicates with the UI through a small, framework-agnostic interface (CommitStoreNotifying). Each platform adapts it to the local notification mechanism (Adapter pattern):

Signal

Purpose

notifyDatabaseDidOpen

Database opened successfully

notifyDatabaseDidClose

Database closed

notifyDatabaseWillReset

Reset is about to occur

notifyStateDidChange

State changed (undo/redo available)

notifyDispatchError

Error occurred during dispatch

The bridges are platform-specific: NSNotificationCenter (AppKit), Qt signals (Qt C++ and PySide), or any other observer mechanism. The Application Context never imports a UI framework — it speaks CommitStoreNotifying and lets a per-platform adapter publish.

The dual-layer contract

The Commit Engine guarantees structural integrity — typed mutations land cleanly in the DAG, the commit graph stays consistent, undo/redo behaves deterministically. It does not guarantee semantic integrity — that the application’s domain invariants are upheld.

That responsibility belongs to the Application Context. The pattern asks the application to validate its own domain rules at consumption time, not at write time, because a commit may have been authored elsewhere (different version, different author) and only discovered locally on sync.

See The Dual-Layer Contract for the full rationale and the where-to-validate decision tree.

Generic versus domain-specific instances

The same Model produces two distinct kinds of application:

  • Generic instancescdbe.py and web-cdbe. No DSM model, no Kibo output, no business logic. They open any CommitDatabase and drive its commit DAG through the dynamic introspection API of the runtime. Their value is universality.

  • Domain-specific instancesge-py, ge-qml, and any third-party Commit Application. They have a DSM model that defines the domain (graphs, materials, schedules, …), Kibo-generated typed accessors, and hand-written business logic that encodes the domain rules. Their value is fidelity to a specific use case.

Both instantiate the same Model. The difference is what fills layers 3 and 4: glue over the dynamic API, or generated typed code over the domain.

Pattern lineage

The Commit Application Model is a disciplined synthesis of three established application patterns:

Pattern

What it contributes

Redux / Flux

Single store, dispatch, unidirectional flow

CQRS (CQS-leaning)

Read interface and mutation interface separated

Observer (Adapter)

Notifier bridge to the platform UI

What’s specific is the integration with the Commit Engine: persistence, history, branching, and multi-author convergence are intrinsic rather than bolted on.

Implementing your own

To build a new Commit Application:

  1. Define your domain in DSM.

  2. Generate the typed infrastructure with Kibo and the kibo-template-viper pack.

  3. Write your Application Context: own a CommitStore, expose your domain state and dispatch surface, hold your business logic.

  4. Adapt CommitStoreNotifying to your UI framework.

  5. Build your UI on top of the notifications.

The walkthroughs that follow this page show this in real code, on three different presentation tiers.