Skip to content

Latest commit

 

History

History
172 lines (141 loc) · 6.9 KB

File metadata and controls

172 lines (141 loc) · 6.9 KB

Design specs

These files are the authoritative design reference for morph. They carry the reasoning, invariants, and rejected alternatives that the code itself does not record. Per AGENTS.md, read a subsystem's spec before changing that subsystem — and when a change invalidates a spec, the spec is what gets updated, not the other way round.

This page is a map, not a summary. It exists so that "read the spec first" names a specific file. For the narrative walkthrough, see docs/ARCHITECTURE.md; if you have not built anything on morph yet, start further back, with the tutorial in docs/GETTING-STARTED.md — the three documents form a path: tutorial → architecture narrative → these specs.

How an action reaches a model

The client half is identical in both deployments. The two diverge at IBackend::execute, and it is worth knowing which side you are looking at: ActionDispatcher is server-side only. LocalBackend never consults it — it posts the action's own localOp on the model's strand.

sequenceDiagram
    participant GUI as GUI / call site
    participant H as BridgeHandler<Model>
    participant B as Bridge
    participant BE as IBackend
    participant M as Model
    participant CB as cbExec

    GUI->>H: execute(action)
    H->>B: executeVia<Model, Action>(binding, action, cbExec)
    B->>BE: execute(ModelId, ActionCall, cbExec)

    alt LocalBackend (in-process)
        BE->>M: post call.localOp on the model's strand,<br/>under ScopedContext
    else RemoteServer (over the wire)
        BE->>BE: serialise ActionCall, send "execute"
        BE->>M: dispatchMessage looks the model up on a pool thread,<br/>then ActionDispatcher::dispatch on the model's strand,<br/>under ScopedContext
        BE->>BE: reply resolves the pending callId
    end

    M-->>CB: Completion&lt;R&gt; resolves
    CB-->>GUI: .then / .onError run on cbExec
Loading

Two things this diagram is making explicit, because both are easy to get wrong:

  • cbExec is independent of everything above it. Where the model ran has no bearing on where your .then runs; the callback executor you passed decides that. See core/executor.md.
  • The model's strand is what serialises access, not a mutex in the model. See concurrency_and_lifetimes.md.

Deployment topologies

flowchart LR
    subgraph local["In-process — LocalBackend"]
        direction LR
        A1[GUI] --> B1[Bridge] --> L[LocalBackend] --> M1[Model]
    end

    subgraph remote["Client/server — over a real socket"]
        direction LR
        A2[GUI] --> B2[Bridge] --> W[QtWebSocketBackend]
        W -. WebSocket .-> WS[QtWebSocketServer]
        WS --> RS[RemoteServer] --> AD[ActionDispatcher] --> M2[Model]
    end

    subgraph sim["Tests — no transport"]
        direction LR
        A3[Test] --> B3[Bridge] --> S[SimulatedRemoteBackend] --> RS2[RemoteServer] --> M3[Model]
    end
Loading

SimulatedRemoteBackend exercises the same serialise/dispatch path as the socket transport without a socket, which is why a bug that only appears remotely is usually reproducible in a plain unit test. Per-backend API details are in core/backend.md; the behavioural differences between the two, collected in one table, are in core/locality.md.

Start here — specs by the question they answer

How a call flows core/bridge.md · core/backend.md · core/registry.md · core/completion.md · core/wire.md · core/locality.md

Threading and lifetime core/executor.md · concurrency_and_lifetimes.md · core/callback_scope.md · core/shared_instances.md

When things go wrong error_handling.md · core/logger.md · core/observability.md

Working offline offline/offline.md · journal/journal.md · core/file_io_ops.md — the injectable file-I/O seam both the journal and the offline queue write through, and the one to read before writing a durability test

Identity session/session.md · security.md

Schema-driven UI forms/forms.md · forms/views.md · forms/choice.md · forms/widget_hints.md · forms/workflows_navigation.md

Exact values on the wire util/rational.md · util/quantity_type.md · util/datetime.md · util/tagged.md — the newtype wrapper that stops two unrelated protocol scalars sharing one underlying type from being interchangeable

Process VERSIONING.md · testing_strategy.md

Reading order

Adding a model and its actions. core/bridge.md for how a handler binds and dispatches → core/registry.md for what registration macros actually do → core/completion.md for the result type you return → core/wire.md if the action must cross a socket, which constrains the field types you may use. Then ARCHITECTURE.md's "Adding a new model and actions" for the concrete steps.

Debugging a dispatch that misbehaves. Establish which side ran the action (core/locality.md tabulates the behaviour differences that produce most of these bugs; core/backend.md has the per-backend detail) → then core/executor.md and concurrency_and_lifetimes.md if the symptom is a race, a use-after-free, or a callback on an unexpected thread → error_handling.md if an error arrived in the wrong shape or not at all.

pinned_facts.toml

pinned_facts.toml is not prose. It is the spec↔code drift guard: each entry pins a mechanical fact some spec states in words (a buffer size, a limit, a default) to the real symbol, and tests/test_pinned_facts.cpp fails if they diverge, while scripts/check_spec_citations.sh fails if the citing spec stops mentioning it. Change a pinned value only in the same commit that changes both the code and the prose. See CONTRIBUTING.md under "Quality gates".

Keeping this file cheap

A new spec adds one line to the map above. The diagrams change only when the call path itself changes — if you find yourself restating what a spec says, that belongs in the spec.