Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
863c681
docs: server push — actions declare what they change, the server tell…
Yaraslaut Oct 16, 2026
62763fe
docs: spec 8 — address the first review round
Yaraslaut Oct 16, 2026
6ec60b0
docs: spec 8 — address the second review round
Yaraslaut Oct 16, 2026
eb9f945
docs: spec 8 — address the third review round
Yaraslaut Oct 16, 2026
35039b0
docs: spec 8 — address the fourth review round
Yaraslaut Oct 16, 2026
1c64354
docs: spec 8 — address the fifth review round
Yaraslaut Oct 16, 2026
229c22d
docs: spec 8 — address the sixth review round
Yaraslaut Oct 16, 2026
e349152
docs: spec 8 — address the seventh review round's minor findings
Yaraslaut Oct 16, 2026
01ff972
docs: spec 8 follows master's three-rung ladder and explorer; one PR,…
Yaraslaut Oct 18, 2026
eadecff
tests: table tests use std::iota, which every libc++ has
Yaraslaut Oct 18, 2026
9add7e6
core: a registry of record types, with keys, derivation and private keys
Yaraslaut Oct 18, 2026
aa2b4fd
core: complete the record registry's documentation
Yaraslaut Oct 18, 2026
404c61d
core: an action's registration records where its result holds which t…
Yaraslaut Oct 18, 2026
cd80477
core: a result slot remembers its action type, and a second type unde…
Yaraslaut Oct 18, 2026
cb281df
core: actions declare their effects; matched and validated on first read
Yaraslaut Oct 18, 2026
cbd9445
core: identical record registrations are one registration; stricter e…
Yaraslaut Oct 18, 2026
dde59f5
core: derive an execute's changes from its effects and its result
Yaraslaut Oct 18, 2026
0a1dac7
core: a server-initiated event kind, routed before reply matching
Yaraslaut Oct 18, 2026
b3b5e43
core: RemoteServer writes events to a connection, and holds them back…
Yaraslaut Oct 18, 2026
0dfdc2c
net: per-connection byte backlog, drain callback, events and the conn…
Yaraslaut Oct 18, 2026
ab1a1c5
net: a connection gets its writer after the handshake; backlog consta…
Yaraslaut Oct 18, 2026
e15681c
qt: per-connection backlog, events, and a send timeout like net's
Yaraslaut Oct 18, 2026
c8c96ea
qt: the backlog over TLS counts encrypted bytes
Yaraslaut Oct 18, 2026
a3980e3
core: SimulatedRemoteBackend carries events; one conformance case on …
Yaraslaut Oct 18, 2026
92eca96
ui: the catalog carries each action's effects and records
Yaraslaut Oct 18, 2026
7c8899b
core, ui: negotiate events in ui-hello; core learns it through Connec…
Yaraslaut Oct 18, 2026
0a8439b
core: the watch, unwatch and changes bodies
Yaraslaut Oct 18, 2026
ee9fd2e
core: a change never carries more keys than a client accepts
Yaraslaut Oct 18, 2026
4ee5617
core: effects.hpp names no identifier after a Qt keyword
Yaraslaut Oct 18, 2026
4d8d210
core: connections watch record types and keyed instances
Yaraslaut Oct 18, 2026
9c0a671
core: watch admission keys on the token, checks instance types, and c…
Yaraslaut Oct 18, 2026
94d8dbb
core: a successful execute tells every watching connection what changed
Yaraslaut Oct 18, 2026
6d5211e
core: a connection's watches drop when its principal changes
Yaraslaut Oct 18, 2026
af29529
core: every execute compares its principal with the connection's, eve…
Yaraslaut Oct 18, 2026
92eed1c
core: fan-out re-authorises each watch by the rule that admitted it
Yaraslaut Oct 18, 2026
134c215
core: a connection whose watches the server drops is owed a resync
Yaraslaut Oct 18, 2026
31a533f
core: coalesce a connection's changes for 50 ms or 256 keys
Yaraslaut Oct 18, 2026
d22c3dc
core: a principal change discards changes queued for the previous pri…
Yaraslaut Oct 18, 2026
69dfd01
ui: a change marked during a fetch is kept until the next fetch
Yaraslaut Oct 18, 2026
2d1e705
ui: every reply applies its action's effects to the cache
Yaraslaut Oct 18, 2026
fdebb7f
ui: the cache applies change events and resync
Yaraslaut Oct 18, 2026
ffc6a0e
core, tests: build on libstdc++ and MSVC, install every detail header…
Yaraslaut Oct 18, 2026
c89a731
tests: the net backlog test reads through a 256 KiB window, not 2 KiB
Yaraslaut Oct 18, 2026
a9cee7e
ui: the session watches what its cache holds, before it fetches
Yaraslaut Oct 18, 2026
72f8294
ui, examples: the generic client re-negotiates and re-watches on ever…
Yaraslaut Oct 18, 2026
2814880
core: a record registered without options compiles under GCC
Yaraslaut Oct 18, 2026
e43964c
ui, qt, tests: pass clang-tidy on the cache's effect and event paths
Yaraslaut Oct 18, 2026
4654553
core: the result walker compiles under GCC for an aggregate with no m…
Yaraslaut Oct 18, 2026
9cbd29a
ui: documents are checked against action effects
Yaraslaut Oct 18, 2026
37fb712
tests: the net backlog test waits for every execute before the drain
Yaraslaut Oct 18, 2026
bd79aa6
ui: the explorer shows what an action changes and which records it re…
Yaraslaut Oct 18, 2026
5ad80d1
tests: one client's mutation updates another client's screen over net
Yaraslaut Oct 18, 2026
cf7123c
examples(bookmarks): actions declare their effects; screens drop refr…
Yaraslaut Oct 18, 2026
6e5cc10
examples(bank): actions declare their effects; screens drop refresh d…
Yaraslaut Oct 18, 2026
4d529cd
examples(lims): actions declare their effects; screens drop refresh d…
Yaraslaut Oct 18, 2026
cb34db9
ui: an MCP adapter runs agent tool calls through an ordinary connection
Yaraslaut Oct 18, 2026
6e50258
ui: the catalog states the records derived from what an action changes
Yaraslaut Oct 18, 2026
9dd36d0
ui, examples, ci: pass clang-tidy and the coverage gate on the cache,…
Yaraslaut Oct 19, 2026
0b05f18
examples(bank): --mcp-stdio serves the MCP adapter beside the WebSock…
Yaraslaut Oct 19, 2026
a1b6c3d
examples(bank): format the server's MCP thread
Yaraslaut Oct 19, 2026
4074012
ui: ui/2 removes invalidates, patch and refreshOn; ui/1 documents sti…
Yaraslaut Oct 19, 2026
3e66a4c
ui, core: clang-tidy fixes; the MCP adapter builds its JSON without o…
Yaraslaut Oct 19, 2026
0972e68
effects: refuse a derivedFrom source that is not a registered record
Yaraslaut Oct 19, 2026
40eb522
tests: the watch table on its own and the channel's edge cases; moved…
Yaraslaut Oct 19, 2026
81fdc2f
ui: one vocabulary; ui/1 refuses invalidates, patch and refreshOn
Yaraslaut Oct 19, 2026
156c1b7
docs: a guide to keeping every screen current, and the docs that poin…
Yaraslaut Oct 19, 2026
eb52b96
Address code review feedback
Yaraslaut Oct 19, 2026
5a0a61a
coverage: move the document.hpp allowlist hints to their current lines
Yaraslaut Oct 19, 2026
66ee0b0
Merge branch 'master' into docs/server-push-spec
Yaraslaut Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ So, before claiming something works:
State results as measured or as inferred, and never round the second up to the
first.

## Adding or changing an action

An action that changes data declares what it changes (`BRIDGE_REGISTER_EFFECTS`, beside its
`BRIDGE_REGISTER_ACTION`), and screens never say what to refresh. Work through the checklist in
[`docs/KEEPING-SCREENS-CURRENT.md`](docs/KEEPING-SCREENS-CURRENT.md) for every new or changed action.

## Design specs (`docs/spec/`)

One file per public type or subsystem. There is **no size limit** on spec
Expand Down
14 changes: 14 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,13 @@ target_sources(morph
include/morph/core/timeout_scheduler.hpp
include/morph/core/model.hpp
include/morph/core/registry.hpp
include/morph/core/records.hpp
include/morph/core/effects.hpp
include/morph/core/change_types.hpp
include/morph/core/changes.hpp
include/morph/core/backend.hpp
include/morph/core/envelope_transport.hpp
include/morph/core/connection_writer.hpp
include/morph/core/remote.hpp
include/morph/core/bridge.hpp
include/morph/core/wire.hpp
Expand Down Expand Up @@ -510,6 +515,7 @@ target_sources(morph
include/morph/ui/service_client.hpp
include/morph/ui/shell_frame.hpp
include/morph/ui/explorer.hpp
include/morph/ui/agent.hpp
include/morph/ui/preferences.hpp
include/morph/ui/preference_sync.hpp
include/morph/ui/host_vocabulary.hpp
Expand Down Expand Up @@ -601,6 +607,9 @@ target_sources(morph
include/morph/core/detail/owner_probe.hpp
include/morph/core/detail/reply_router.hpp
include/morph/core/detail/subscription_registry.hpp
include/morph/core/detail/result_walker.hpp
include/morph/core/detail/watch_table.hpp
include/morph/core/detail/change_coalescer.hpp
include/morph/forms/detail/session_common.hpp
include/morph/forms/detail/schema_name.hpp
include/morph/forms/engine/detail/text.hpp
Expand Down Expand Up @@ -1041,6 +1050,7 @@ if(MORPH_BUILD_QT)
include/morph/qt/qt_tls.hpp
include/morph/qt/qt_websocket_server.hpp
include/morph/qt/qt_websocket_backend.hpp
include/morph/qt/detail/outbound_ledger.hpp
)

# Compiled implementation (QObject subclass needs MOC via a .cpp file)
Expand Down Expand Up @@ -1154,6 +1164,10 @@ if(MORPH_BUILD_EXAMPLES)
if(MORPH_BUILD_QT_QUICK OR MORPH_BUILD_TUI)
add_subdirectory(examples/client)
endif()
elseif(TARGET ladder_bank_server)
# The bank server serves bank's screens, so it needs them without the generic examples; their tests, which use
# the generic app, are built only with them.
add_subdirectory(examples/bank/screens)
endif()

# ── Documentation ───────────────────────────────────────────────────────────
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,9 +530,10 @@ in [`docs/spec/`](docs/spec) before relying on any of these in production:
- **`Completion<T>` is a leaf callback primitive**, not a composable future: one
handler per outcome, no `T→U` chaining, no `co_await`, no cancellation.
- **Instance subscriptions are best-effort and in-process.** `subscribe<R>`
fans out to handlers on the same `Bridge`; there is no server-initiated push,
so two separate clients sharing an instance do not see each other's results
until they ask again. No replay, no durability, no coalescing.
fans out to handlers on the same `Bridge`, with no replay and no durability.
Across clients, screens stay current through declared effects, which tell
every watching connection what changed but not the new values
([`docs/KEEPING-SCREENS-CURRENT.md`](docs/KEEPING-SCREENS-CURRENT.md)).
- **Exact numbers are fixed-width.** `Rational` is an `int64` pair; `+`/`-`/`*`
*saturate* at `±INT64_MAX/1` and log at `error` rather than returning an error
value, so an out-of-envelope result is clamped and inexact rather than exact —
Expand Down Expand Up @@ -561,7 +562,10 @@ in [`docs/spec/`](docs/spec) before relying on any of these in production:

- [`docs/GETTING-STARTED.md`](docs/GETTING-STARTED.md) — the step-by-step
tutorial: build one small app end to end and understand why each piece
exists. Read this before the two below.
exists. Read this before the others below.
- [`docs/KEEPING-SCREENS-CURRENT.md`](docs/KEEPING-SCREENS-CURRENT.md) — how
screens refresh after an action, on every client and for AI agents: records,
effects, the checklist for a new action, and every error with its fix.
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — namespace map, layer diagram,
wire protocol, deployment topologies, and design rationale.
- [`docs/spec/`](docs/spec) — the authoritative per-type/subsystem design specs.
Expand Down
37 changes: 36 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ The public surface is split per topic so callers always know whether a name is p
| `morph::log` | Configurable logging | `LogLevel`, `setLogger`, `setLogLevel`, `getLogLevel`, `logDebug`, `logInfo`, `logWarn`, `logError` |
| `morph::exec` | Executor primitives | `IExecutor`, `ThreadPoolExecutor`, `MainThreadExecutor` |
| `morph::async` | Async result handle | `Completion<T>` |
| `morph::model` | Model & action traits | `ModelTraits<>`, `ActionTraits<>`, `ActionValidator<>`, `ActionLogPolicy<>`, `Loggable` |
| `morph::model` | Model & action traits | `ModelTraits<>`, `ActionTraits<>`, `ActionValidator<>`, `ActionLogPolicy<>`, `Loggable`, `EffectsRegistrationError` |
| `morph::record` | Result types that are shared data (see `docs/KEEPING-SCREENS-CURRENT.md`) | `BRIDGE_REGISTER_RECORD`, `key<&T::m>`, `keyless`, `derivedFrom<S...>`, `privateKeys`, `RecordInfo` |
| `morph::effects` | What an action changes, and the changes an execute made | `BRIDGE_REGISTER_EFFECTS`, `updates<R>`, `changes<R>`, `changesInstance`, `none`, `Effect`, `ActionRecord`, `effectsOf`, `recordsOf`, `recordNamed`, `actionsHolding`, `deriveChanges`, `Changes`, `kMaxChangeKeys` |
| `morph::backend` | Pluggable backends | `LocalBackend`, `RemoteServer`, `SimulatedRemoteBackend` |
| `morph::bridge` | Bridge between handler and backend | `Bridge`, `BridgeHandler<M>` |
| `morph::offline` | Connectivity + replay | `NetworkMonitor`, `NetworkMonitorConfig`, `IOfflineQueue`, `QueueItem`, `OfflineQueueFullError`, `InMemoryOfflineQueue`, `FileOfflineQueue`, `FileOfflineQueueError`, `SqliteOfflineQueue`, `SqliteOfflineQueueError`, `SyncWorker` (incl. `DeadLetterSink`), `SyncResult`, `ReconnectCoordinator`, `ReconnectOutcome`, `ReconnectCoordinatorConfig` |
Expand Down Expand Up @@ -420,6 +422,14 @@ struct MyModel {

BRIDGE_REGISTER_MODEL (MyModel, "MyModel")
BRIDGE_REGISTER_ACTION(MyModel, MyAction, "MyAction")
```

An action that changes data also says what it changes, so every screen showing it refreshes
(`docs/KEEPING-SCREENS-CURRENT.md`):

```cpp
BRIDGE_REGISTER_RECORD(MyRow, "MyRow") // a result type that is shared data
BRIDGE_REGISTER_EFFECTS(MyAction, ::morph::effects::changes<MyRow>) // beside the action's registration
```

3. Use from the GUI (same code for local and remote):
Expand Down Expand Up @@ -514,6 +524,26 @@ sidebar.execute(Deposit{.amountMinor = 5000});
| **Callback thread** | Always the executor passed at handler construction. |


## Keeping screens current across clients

`subscribe<R>` reaches handlers on one `Bridge`. What keeps every client's screens current, and an agent's changes
visible to them, is declared effects:

- `BRIDGE_REGISTER_RECORD` names a result type that is shared data, with its key.
- `BRIDGE_REGISTER_EFFECTS`, beside an action's registration, says which records it changes (`updates<R>` when the
reply carries the changed rows, `changes<R>` when it does not, `changesInstance`, or `none`).
- After a successful execute, `RemoteServer` derives the changes from the effects and the result, and sends an
`event` to every connection watching a changed record, after re-authorising each watch. A UI client watches the
records its query cache holds, before it fetches them, and applies a change by patching or refetching the entries
that hold the record. The caller applies its own reply's effects at once.
- Screens declare no refreshes: a document naming `invalidates`, `patch` or `refreshOn` is refused, and so is a query
whose action declares effects.
- `morph::ui::McpAdapter` (`ui/agent.hpp`) lets an AI agent drive the same server over MCP, as one more connection.

The user guide is [`KEEPING-SCREENS-CURRENT.md`](KEEPING-SCREENS-CURRENT.md); the rules are in
`docs/spec/core/registry.md` ("Effects"), `docs/spec/core/backend.md` ("Fan-out"), `docs/spec/ui/interpreter.md` and
`docs/spec/ui/document.md`.

## Exact values, units, and schema-driven forms

Three headers extend actions from "any aggregate" to *self-describing*
Expand Down Expand Up @@ -657,6 +687,10 @@ folder.
| `core/completion.hpp` | `CompletionState<T>` (detail) + `Completion<T>` (public) — result handle |
| `core/model.hpp` | `IModelHolder`, `ModelHolder<T>`, `ModelFactory`, `IBackendChangedSink`, `BackendChangedNotifiable` — type-erased model storage; `IModelHolder::attachActionLog`/`hasActionLog`/`recordIfAttached` (`morph::model::detail::`) |
| `core/registry.hpp` | `ModelTraits<>`, `ActionTraits<>`, `ActionValidator<>`, `ActionLogPolicy<>`, `Loggable` (public) + `ActionDispatcher` (also tracking each action's `coalesce` policy), `ModelRegistryFactory`, `defaultDispatcher()`, `defaultRegistry()`, `ParseError`, `registerModelOnce`, `registerActionOnce`, `actionLoggable<A>()` (detail). Registration macros `BRIDGE_REGISTER_MODEL`, `BRIDGE_REGISTER_ACTION` (optional 4th `Loggable` argument), `BRIDGE_REGISTER_VALIDATOR` are defined here at file scope. |
| `core/records.hpp` | `BRIDGE_REGISTER_RECORD` and its options `key<&T::m>`, `keyless`, `derivedFrom<S...>`, `privateKeys`; `RecordInfo` (`morph::record::`) — result types that are shared data. See `docs/spec/core/registry.md`, "Record registry". |
| `core/effects.hpp` | `BRIDGE_REGISTER_EFFECTS` and `updates<R>`, `changes<R>`, `changesInstance`, `none`; `Effect`, `ActionRecord`, `effectsOf`, `recordsOf`, `recordNamed`, `actionsHolding` (`morph::effects::`); `EffectsRegistrationError` (`morph::model::`) — what each action changes, matched with the records its result holds and validated on the first read. See `docs/spec/core/registry.md`, "Effects". |
| `core/changes.hpp`, `core/change_types.hpp` | `deriveChanges`, `Changes`, `RecordChange`, `InstanceChange`, `kMaxChangeKeys` (`morph::effects::`) — the changes a successful execute made. |
| `core/connection_writer.hpp` | `ConnectionWriter`, `kEventBacklogHigh`, `kEventBacklogLow` (`morph::backend::`) — how `RemoteServer` writes events to a connection, and the backlog above which it holds them back. |
| `core/backend.hpp` | `LocalBackend` (public) + `ActionCall`, `IBackend` (detail), including the non-breaking `registerModelWithContext()` default method |
| `core/remote.hpp` | `RemoteServer` (configured by `ServerConfig`, incl. `logProvider`), `SimulatedRemoteBackend` (`morph::backend::`) |
| `core/bridge.hpp` | `Bridge`, `BridgeHandler<M>` (public) + `HandlerBinding` (carrying `contextKey`), `MemberPointerTraits` (detail) |
Expand Down Expand Up @@ -736,6 +770,7 @@ folder.
| `ui/expression.hpp` | `Expr`, `ExprKind`, `FunctionId`, `parseExpression`, `NameResolver`, `PermissiveResolver`, `checkExpression`, `Environment`, `StaticEnvironment`, `FormatContext`, `UnitTable`, `UnitInfo`, `evaluate`, `walk`, `isName`, `functionNames` — the `expr/1` language. See `docs/spec/ui/expressions.md`. |
| `ui/document.hpp` | `Document`, `ScopeDecl` and the declaration structs, `Command`, `CommandKind`, `DocNode`, `Component`, `CustomizePoint`, `Catalog`, `ModelInfo`, `ActionInfo`, `LoadOptions`, `loadDocument`, `loadDocumentText`, `nodeKinds` — the document model and its load-time validation. See `docs/spec/ui/document.md`. |
| `ui/interpreter.hpp` | `Interpreter`, `Screen`, `Session`, `SessionOptions`, `Dispatcher`, `ModelChannel`, `CallOptions`, `BridgeDispatcher`, `InterpreterHost`, `NavigateRequest`, `InterpreterOptions`, `MountOptions`, `BorrowedModel` (`morph::ui::`) — mounts a loaded document: scopes, the session's query cache, mutations, relations and commands, lowered to the view tree. See `docs/spec/ui/interpreter.md`. |
| `ui/agent.hpp` | `McpAdapter`, `McpOptions`, `serveStdio` and the JSON-RPC error codes (`morph::ui::`) — an AI agent over MCP, as one more connection to the server. See `docs/spec/ui/agent.md`. |
| `ui/author.hpp` | `Handle`, `AnyHandle`, `Body`, `ModelHandle`, `QueryHandle`, `MutationHandle`, `DeclScope`, `Scope`, `Command`, `Commands`, `Node`, `Nodes`, a builder per node kind and command, `Screen`, `screen`, `ScreenRegistry`, `screenRegistry`, `CatalogFacts`, `ModelFacts`, `ActionFacts`, `checkAccessibility`, `MORPH_REGISTER_SCREEN` (`morph::ui::author::`) — server-side authoring: typed builders that write a screen's document, and its registration with the load-time and accessibility rules. See `docs/spec/ui/author.md`. |
| `ui/view.hpp` | `Node`, `NodeData`, `Prop<T>`, `SlotId`, `SlotBinding`, `slot()`, `Key`, `Common`, `Accessibility`, `KeyBinding`, `FieldState`, `MenuItem`, `Sizing`, `LayoutHints`, every node kind and its builder, `switchOn`, `forEach`, `table` (`morph::ui::`) — the immutable view tree whose properties are constants or slots. See `docs/spec/ui/view_tree.md`. |
| `ui/backend.hpp` | `Widget`, `ContainerWidget`, `FieldWidget`, one widget interface per kind, `MenuEntry`, `IViewBackend` — the retained widgets a renderer implements. See `docs/spec/ui/backend_contract.md`. |
Expand Down
50 changes: 40 additions & 10 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,10 +638,10 @@ abridged):
auto const marks = screen.model<BookmarkModel>("marks", {.instance = ui::Instance::Private});
auto const list = ui::element("list");
auto const bookmarks = screen.query<ListBookmarks>("bookmarks", marks, {});
auto const archive = screen.mutation<BulkEdit>("archive", marks, {.invalidates = {bookmarks, tags}});
auto const archive = screen.mutation<BulkEdit>("archive", marks);
auto const rows = bookmarks.value().at(&ListBookmarksResult::bookmarks);
return ui::column({
ui::form<CreateBookmark>(marks, {.onSuccess = refreshed}).id(ui::element("createForm")).testId("createForm"),
ui::form<CreateBookmark>(marks).id(ui::element("createForm")).testId("createForm"),
ui::table(rows, {.key = ui::row("id"), /* title and URL columns */, .selection = ui::Selection::Multiple})
.id(list).testId("bookmarks"),
ui::button({.label = "Archive selected",
Expand All @@ -652,8 +652,9 @@ return ui::column({
```

The screen decides nothing about the domain. It names the model, the query
that lists bookmarks, the mutation, and when the list refetches; every rule
about what a bookmark may be stays in `BookmarkModel`. Archiving a selection is
that lists bookmarks and the mutation; every rule about what a bookmark may be
stays in `BookmarkModel`, and nothing on the screen says when the list
refetches. Archiving a selection is
one `BulkEdit` built from the table's own selection, so it is one transaction
in the model however many rows are selected.

Expand All @@ -676,6 +677,32 @@ REQUIRE(harness.submitForm("createForm"));
REQUIRE(harness.waitForText("count", "1 bookmarks"));
```

### What refreshes after an action

The action says what it changes, beside its registration, and every screen
showing that data refreshes: on this client, on every other connected client,
and when an AI agent runs the action over MCP. Two declarations do it
([`models/bookmark_model.hpp`](../examples/bookmarks/include/bookmarks/models/bookmark_model.hpp),
[`dto/bookmark_dto.hpp`](../examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp)):

```cpp
// A result type that is shared data, keyed by its `id` member
BRIDGE_REGISTER_RECORD(bookmarks::BookmarkSummary, "Bookmarks_Bookmark", ::morph::record::privateKeys)

// What the action changes: archiving moves bookmarks between lists, and changes the tag counts
BRIDGE_REGISTER_ACTION(bookmarks::BookmarkModel, bookmarks::BulkEdit, "BulkEdit", ::morph::model::Loggable::No)
BRIDGE_REGISTER_EFFECTS(bookmarks::BulkEdit, ::morph::effects::changes<bookmarks::BookmarkSummary>,
::morph::effects::changes<bookmarks::BookmarkView>,
::morph::effects::changes<bookmarks::TagSummary>)
```

`changes<R>` refetches every query holding an `R`; `updates<R>` is for an
action whose reply carries the changed rows, which the caller's screens show
at once, before any fetch; `none` is for an action that changes nothing. A query whose
action declares effects is refused, because a query must only read. How to
choose, the checklist for a new action and every error message with its fix
are in [`docs/KEEPING-SCREENS-CURRENT.md`](KEEPING-SCREENS-CURRENT.md).

## 10. Forms from the compiled action type

`morph::forms::schemaJson<A>()` derives a JSON Schema from the action type
Expand Down Expand Up @@ -856,9 +883,10 @@ client.
A guide that only sells is worth less than one that tells you when to stop.
Read [`docs/spec/`](spec/) before relying on any of these; the short version:

- **It is not a general-purpose RPC framework.** Request/response only. No
streaming, no bidirectional channels, no server-initiated push. bookmarks'
`GetChangesSince` is the polling answer a client uses instead.
- **It is not a general-purpose RPC framework.** Request/response, plus one
kind of server-initiated message: the change notifications that keep
screens current ([`docs/KEEPING-SCREENS-CURRENT.md`](KEEPING-SCREENS-CURRENT.md)).
No streaming and no general bidirectional channel.
- **`Completion<T>` is a leaf callback primitive, not a future.** No `T→U`
chaining, no `co_await`, and no cancellation; lifetime binding is the opt-in
`CallbackScope` of section 8.
Expand All @@ -871,9 +899,9 @@ Read [`docs/spec/`](spec/) before relying on any of these; the short version:
the default; a server that configures none of them assumes a trusted,
authenticated transport and is not a hardened public-internet server.
- **Instance subscriptions are best-effort and in-process.** `subscribe<R>`
fans out to handlers on the same `Bridge`; two separate clients sharing an
instance do not see each other's results until they ask again. No replay, no
durability, no coalescing.
fans out to handlers on the same `Bridge`, with no replay and no durability.
Across clients, what keeps screens current is declared effects, which tell
every connection what changed but not the new values.
- **Exact numbers are fixed-width.** `Rational` is an `int64` pair; `+`/`-`/`*`
can overflow rather than returning an error, and high decimal precision
shrinks the representable magnitude.
Expand Down Expand Up @@ -905,6 +933,8 @@ You now have a question rather than a topic, which is what the map is for.
| who is allowed to do what? | `docs/spec/security.md`, `docs/spec/session/session.md` |
| what serialises access to my model? | `docs/spec/concurrency_and_lifetimes.md` |
| how do I keep several screens on one model instance? | `docs/spec/core/shared_instances.md` |
| how do screens refresh after an action, and what do I declare? | [`docs/KEEPING-SCREENS-CURRENT.md`](KEEPING-SCREENS-CURRENT.md) |
| how does an AI agent drive my app? | `docs/spec/ui/agent.md`, and bank's `--mcp-stdio` ([`examples/bank/README.md`](../examples/bank/README.md)) |
| how do I generate a form? | `docs/spec/forms/forms.md`, and [`examples/forms`](../examples/forms) |
| how do I test all this? | [`examples/TESTING.md`](../examples/TESTING.md) |
| what are the rules for an app built on morph? | [`examples/IMPLEMENTATION.md`](../examples/IMPLEMENTATION.md) |
Expand Down
Loading
Loading