Skip to content
Back to top
View this page

# Application Architecture

This chapter describes how to structure applications built with DSM and Viper. The architecture separates concerns into layers, enabling code reuse across platforms and languages.

## The 3-Tier Architecture

Applications built with Viper follow a 3-tier pattern:

    ┌────────────────────────────────────────────────────────────┐
    │  TIER 1: PRESENTATION (Platform-Specific UI)               │
    │  AppKit (macOS) / Qt (cross-platform) / PySide (Python)    │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  TIER 2: LOGIC (Context + Bridges + Components)            │
    │  Application Context (orchestration), generated pools,     │
    │  hand-written business code                                │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  TIER 3: DATA (Viper Runtime)                              │
    │  CommitDatabase, CommitStore, persistence, dsviper binding │
    └────────────────────────────────────────────────────────────┘

### Tier Responsibilities

Tier | Responsibility | Platform-Specific?
--- | --- | ---
Presentation | UI framework, windows, dialogs | Yes
Logic | Application Context, bridges, business components | Partially
Data | Viper runtime (CommitDatabase, CommitStore), persistence | No (shared)

## The Full Application Stack

For business applications with domain logic, the stack expands:

    ┌────────────────────────────────────────────────────────────┐
    │  1. UI Layer                                               │
    │  Platform-specific windows, views, controllers             │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  2. Application Context                                    │
    │  Singleton orchestrator: owns the CommitStore, the         │
    │  generated FunctionPools, and the application's domain     │
    │  state (e.g. current GraphKey)                             │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  3. Bridge Layer                                           │
    │  Pool signatures → Business logic connection               │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  4. Business Logic Layer                                   │
    │  Hand-written domain code (Model_*)                        │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  5. Generated Infrastructure                               │
    │  Kibo output: Data, Commit, Serialization                  │
    └────────────────────────────┬───────────────────────────────┘
                                 │
    ┌────────────────────────────▼───────────────────────────────┐
    │  6. Viper Runtime                                          │
    │  Core C++ engine (CommitStore, CommitDatabase),            │
    │  dsviper Python binding                                    │
    └────────────────────────────────────────────────────────────┘

## Layer Details

### Layer 1: UI

Platform-specific presentation code:

- Window management

- Menu and toolbar actions

- Dialog boxes

- Framework-native widgets

This layer changes for each target platform but follows the same logical structure.

### Layer 2: Application Context

The Context is the application-level singleton that orchestrates the runtime. It is not a subclass of `Viper::CommitStore`: it owns a `CommitStore` (provided by the Viper runtime) and adds the application-specific surface around it — the generated `FunctionPool` / `AttachmentFunctionPool` instances, the domain state (e.g. the current graph), and the action dispatch.

Responsibilities of the Context:

- Own the `CommitStore` (which itself holds the `CommitDatabase` and the current `CommitState`)

- Hold the generated tool / model / attachment `FunctionPools`

- Hold application-level domain state (e.g. `graphKey`)

- Open / close / load the database

- Dispatch user actions through `CommitMutableState`

- Expose framework-agnostic notifications via `CommitStoreNotifying`

    // Context pattern (C++) — see com.digitalsubstrate.ge / GE_Context.hpp
    namespace GE {

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

        // Database lifecycle (owned via the store)
        void use(std::shared_ptr<Viper::CommitDatabase> const & database);
        void close();
        void load();
        static std::shared_ptr<Viper::CommitDatabase> createDatabase(
            std::filesystem::path const & filePath);

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

        // Owned components
        std::shared_ptr<Viper::CommitStore> store;

        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
        GE::Graph::GraphKey graphKey;
    };

    } // namespace GE

Action dispatch always goes through the store’s `CommitMutableState`, so undo/redo and notifications stay coherent:

    void Context::dispatchAction(std::string const & label,
                                 std::shared_ptr<ContextAction> const & action) {
        auto const ms{store->mutableState()};
        action->run(ms);
        store->commitMutations(label, ms);
    }

The previous “Store-as-application-singleton” pattern is superseded: the generic store concerns (database, state, undo/redo, notifications) live in `Viper::CommitStore`, and the application-specific concerns (which pools, which domain state, which actions) live in the application’s `Context`.

### Layer 3: Bridges

Bridges connect generated pool signatures to your business logic:

    // GE_AttachmentFunctionPoolBridges.hpp (GENERATED)
    namespace GE::AttachmentFunctionPoolBridges {
    namespace ModelGraph {

    Graph::VertexKey new_vertex(std::shared_ptr<Viper::AttachmentMutating> const & attachmentMutating,
                                Graph::GraphKey const & graphKey,
                                std::int64_t value,
                                Graph::Position const & position);

    }
    }

    // GE_AttachmentFunctionPoolBridges.cpp (HAND-WRITTEN)
    Graph::VertexKey new_vertex(std::shared_ptr<Viper::AttachmentMutating> const & attachmentMutating,
                                Graph::GraphKey const & graphKey,
                                std::int64_t value,
                                Graph::Position const & position) {
        return Model::Vertex::add(attachmentMutating, graphKey, value, position,
                                  Model::Random::makeColor());
    }

### Layer 4: Business Logic

Your domain-specific code:

    // GE_Model_Vertex.cpp
    VertexKey add(std::shared_ptr<AttachmentMutating> const & attachmentMutating,
                  GraphKey const & graphKey,
                  std::int64_t const & value,
                  Position const & position,
                  Color const & color) {

        auto const vertexKey{create(attachmentMutating, value, position, color)};
        Attachments::Graph_Topology::unionVertexKeys(attachmentMutating, graphKey, {vertexKey});

        return vertexKey;
    }

### Layer 5: Generated Infrastructure

Kibo generates this layer from your DSM:

- Data types (`*_Data.cpp`)

- Commit accessors (`*_Commit.cpp`)

- Serialization (`*_Reader.cpp`, `*_Writer.cpp`)

- Database persistence (`*_Database.cpp`)

- Python proxy

### Layer 6: Viper Runtime

The core C++ engine providing:

- Type and value system

- CommitDatabase persistence

- Stateful commits

- Python binding (dsviper)

## The Notifier Pattern

UI frameworks have different notification mechanisms. The Notifier pattern bridges this gap:

    CommitStore (C++ core)
            │
            ▼
    CommitStoreNotifying (interface)
            │
            ▼
    DSCommitStoreNotifier (framework adapter)
            │
            ▼
    Framework-specific notifications

### Common Notifications

All implementations expose the same signals:

Signal | Purpose
--- | ---
`database_did_open` | Database opened successfully
`database_did_close` | Database closed
`state_did_change` | State changed (undo/redo available)
`definitions_did_change` | Schema updated
`dispatch_error` | Error occurred

## Multi-Language Support

The architecture supports multiple languages:

Layer | C++ Application | Python Application
--- | --- | ---
UI | AppKit/Qt C++ | PySide/Qt Python
Context | C++ singleton (owns CommitStore) | Python class (owns store)
Business | C++ | Python or dsviper
Infrastructure | C++ generated | Python generated
Runtime | Viper C++ (CommitStore, …) | dsviper

### Python Integration

The `dsviper` module exposes the entire Viper runtime, including `CommitStore` and `CommitDatabase`. A Python application’s `Context` follows the same pattern as the C++ one — it instantiates a `CommitStore` and wires the generated pools around it:

    import dsviper

    database = dsviper.CommitDatabase.create("path/to/data.cdb",
                                             "An Application Database")
    store = dsviper.CommitStore.make()
    store.set_database(database)

    # Undo/Redo operations
    if store.can_undo():
        store.undo()

## Design Principles

### Separation of Concerns

Each layer has a single responsibility:

- UI: Only presentation, no business logic

- Context: Orchestration only — owns the `CommitStore` and the pools, does not reimplement them

- Bridges: Connection, not logic

- Business: Domain rules, not infrastructure

- Generated: Infrastructure, not business

### Platform Isolation

Platform-specific code lives only in Layer 1. The other layers are shared or generated.

### Generated vs Hand-Written

Generated (Kibo) | Hand-Written
--- | ---
Data types | Business logic
Serialization | Bridges
Persistence | Store orchestration
Pool marshalling | UI layer

### Minimal Porting Effort

To port to a new platform:

1. Implement the Notifier adapter (~50 lines)

2. Create UI components matching existing patterns

3. Wire up the main window

The business logic and infrastructure remain unchanged.

## Summary

Principle | Implementation
--- | ---
Separation | 6 distinct layers with clear responsibilities
Reuse | Layers 3-6 shared across platforms
Generation | Kibo generates infrastructure (Layer 5)
Multi-language | Same architecture for C++ and Python
Platform isolation | Only Layer 1 is platform-specific

This architecture enables building complex applications that work across platforms while minimizing duplicated code.

## What’s Next

- Python Guide - Use the dsviper Python API

- Toolchain - Master the development tools